Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
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
Download file

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,
    })
}