alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates client src assets mod.rs
//! Loads game assets from the asset directory, laid out like the original jar.
pub mod packs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
/// The `assets` directory next to this crate's sources.
pub const DEFAULT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets");
#[derive(Debug, thiserror::Error)]
pub enum AssetError {
#[error("{0}")]
Pack(String),
#[error("cannot read {path}: {source}")]
Read {
path: PathBuf,
source: std::io::Error,
},
#[error("cannot decode {name}: {source}")]
Decode {
name: String,
source: png::DecodingError,
},
#[error("{name} has unsupported pixel format {format:?}")]
Format {
name: String,
format: png::ColorType,
},
}
#[derive(Clone)]
pub struct RgbaImage {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
}
impl RgbaImage {
/// Pixels as 0xRRGGBB, the form of the original colorizer lookup tables.
pub fn to_rgb(&self) -> Box<[u32]> {
self.pixels
.as_chunks::<4>()
.0
.iter()
.map(|p| u32::from(p[0]) << 16 | u32::from(p[1]) << 8 | u32::from(p[2]))
.collect()
}
}
pub struct Assets {
pub terrain: RgbaImage,
pub grass_colors: RgbaImage,
pub foliage_colors: RgbaImage,
pub sun: RgbaImage,
pub moon: RgbaImage,
pub gui: [RgbaImage; 8],
pub entities: Vec<RgbaImage>,
pub armor: Vec<RgbaImage>,
pub menu: Vec<RgbaImage>,
/// The built-in `gui/items.png` and `misc/dial.png`, which compasses and clocks are drawn
/// from whatever the texture pack.
pub dials: [RgbaImage; 2],
/// `StringTranslate`: `lang/en_US.lang` by key.
pub translations: rustc_hash::FxHashMap<String, String>,
}
/// Entity and effect textures, bound from descriptor slot 11 in this order.
pub const ENTITY_TEXTURES: &[&str] = &[
"mob/pig.png",
"mob/cow.png",
"mob/sheep.png",
"mob/chicken.png",
"mob/zombie.png",
"mob/skeleton.png",
"mob/spider.png",
"mob/creeper.png",
"mob/sheep_fur.png",
"mob/saddle.png",
"mob/char.png",
"item/arrows.png",
"mob/spider_eyes.png",
"particles.png",
"environment/rain.png",
"environment/snow.png",
"misc/footprint.png",
"environment/clouds.png",
"misc/pumpkinblur.png",
"misc/vignette.png",
"gui/trap.png",
"misc/shadow.png",
"misc/water.png",
"item/sign.png",
"mob/pigzombie.png",
"mob/slime.png",
"mob/squid.png",
"art/kz.png",
"mob/ghast.png",
"mob/ghast_fire.png",
"mob/wolf.png",
"mob/wolf_tame.png",
"mob/wolf_angry.png",
"item/boat.png",
"item/cart.png",
"misc/mapbg.png",
"misc/mapicons.png",
"achievement/bg.png",
"gui/slot.png",
"armor/power.png",
];
/// The descriptor slot of an entity texture.
pub const fn entity_texture(name: &str) -> i32 {
let mut index = 0;
while index < ENTITY_TEXTURES.len() {
if const_eq(ENTITY_TEXTURES[index], name) {
return 11 + index as i32;
}
index += 1;
}
panic!("unknown entity texture")
}
const fn const_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
let mut i = 0;
while i < a.len() && i < b.len() && a[i] == b[i] {
i += 1;
}
i == a.len() && i == b.len()
}
/// The first armor texture's slot, after the entity textures.
pub const ARMOR_TEXTURES: i32 = 11 + ENTITY_TEXTURES.len() as i32;
/// The first menu texture's slot, after the ten armor textures.
pub const MENU_TEXTURES: i32 = ARMOR_TEXTURES + 10;
impl Assets {
/// Loads the assets from `dir`, overridden by the files of the texture pack at `pack`.
pub fn load(dir: &Path, pack: Option<&Path>) -> Result<Self, AssetError> {
let mut source = packs::Source::open(dir, pack)?;
let mut image = |name: &str| -> Result<RgbaImage, AssetError> {
let bytes = source.read(name)?;
let image = decode(name, &bytes)?;
if matches!(
name,
"terrain.png" | "misc/grasscolor.png" | "misc/foliagecolor.png"
) && (image.width != 256 || image.height != 256)
{
return Err(AssetError::Pack(format!("{name} must be 256 x 256 pixels")));
}
if name == "font/default.png" && (image.width != image.height || image.width < 128) {
return Err(AssetError::Pack("Invalid font atlas dimensions".into()));
}
Ok(image)
};
Ok(Self {
terrain: image("terrain.png")?,
grass_colors: image("misc/grasscolor.png")?,
foliage_colors: image("misc/foliagecolor.png")?,
sun: image("terrain/sun.png")?,
moon: image("terrain/moon.png")?,
gui: [
image("gui/gui.png")?,
image("gui/icons.png")?,
image("gui/items.png")?,
image("font/default.png")?,
image("gui/inventory.png")?,
image("gui/crafting.png")?,
image("gui/container.png")?,
image("gui/furnace.png")?,
],
entities: ENTITY_TEXTURES
.iter()
.map(|&name| image(name))
.collect::<Result<_, _>>()?,
armor: ["cloth", "chain", "iron", "diamond", "gold"]
.into_iter()
.flat_map(|material| [1, 2].map(|layer| format!("armor/{material}_{layer}.png")))
.map(|name| image(&name))
.collect::<Result<_, _>>()?,
menu: vec![
image("title/mclogo.png")?,
image("gui/background.png")?,
image("pack.png")?,
],
dials: [
builtin(dir, "gui/items.png")?,
builtin(dir, "misc/dial.png")?,
],
translations: ["lang/en_US.lang", "lang/stats_US.lang"]
.into_iter()
.filter_map(|name| source.read(name).ok())
.flat_map(|bytes| {
String::from_utf8_lossy(&bytes)
.lines()
.filter_map(|line| line.split_once('='))
.map(|(key, value)| (key.to_owned(), value.to_owned()))
.collect::<Vec<_>>()
})
.collect(),
})
}
}
/// Decodes the image `name` from `dir`, ignoring any texture pack.
fn builtin(dir: &Path, name: &str) -> Result<RgbaImage, AssetError> {
decode(name, &packs::read_default(dir, name)?)
}
fn decode(name: &str, bytes: &[u8]) -> Result<RgbaImage, AssetError> {
let error = |source: png::DecodingError| AssetError::Decode {
name: name.into(),
source,
};
let mut decoder = png::Decoder::new(Cursor::new(bytes));
decoder.set_limits(png::Limits { bytes: 64 << 20 });
decoder.set_transformations(png::Transformations::normalize_to_color8());
let mut reader = decoder.read_info().map_err(error)?;
let size = reader
.output_buffer_size()
.filter(|&size| size <= 64 << 20)
.ok_or_else(|| AssetError::Pack(format!("{name} is too large")))?;
let mut buffer = vec![0; size];
let info = reader.next_frame(&mut buffer).map_err(error)?;
buffer.truncate(info.buffer_size());
let pixels = match info.color_type {
png::ColorType::Rgba => buffer,
png::ColorType::Rgb => buffer
.as_chunks::<3>()
.0
.iter()
.flat_map(|p| [p[0], p[1], p[2], 255])
.collect(),
png::ColorType::GrayscaleAlpha => buffer
.as_chunks::<2>()
.0
.iter()
.flat_map(|p| [p[0], p[0], p[0], p[1]])
.collect(),
png::ColorType::Grayscale => buffer.iter().flat_map(|&g| [g, g, g, 255]).collect(),
format => {
let name = name.into();
return Err(AssetError::Format { name, format });
}
};
Ok(RgbaImage {
width: info.width,
height: info.height,
pixels,
})
}