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 / packs.rs

use std::ffi::OsString;
use std::fs::{DirEntry, File};
use std::io::{self, Read};
use std::path::{Component, Path, PathBuf};
use std::time::SystemTime;

use super::{AssetError, RgbaImage, builtin, decode};

const MAX_ASSET_BYTES: u64 = 64 << 20;

pub struct Pack {
    pub name: String,
    pub description: [String; 2],
    pub thumbnail: RgbaImage,
}

/// A pack file's name, size and modification time, to notice changes.
pub type Stamp = (OsString, u64, Option<SystemTime>);

pub fn path(directory: &Path, name: &str) -> Result<Option<PathBuf>, AssetError> {
    if name == "Default" {
        return Ok(None);
    }
    let mut components = Path::new(name).components();
    if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() {
        return Err(AssetError::Pack("Invalid texture pack name".into()));
    }
    Ok(Some(directory.join(name)))
}

pub fn list(directory: &Path, defaults: &Path) -> Result<Vec<Pack>, AssetError> {
    let mut packs = vec![Pack {
        name: "Default".into(),
        description: ["The default look of Minecraft".into(), String::new()],
        thumbnail: builtin(defaults, "pack.png")?,
    }];
    let zips = zips(directory).map_err(|source| AssetError::Read {
        path: directory.into(),
        source,
    })?;
    for path in zips.iter().map(DirEntry::path) {
        let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
            continue;
        };
        let opened = Source::open(defaults, Some(&path))
            .and_then(|mut source| Ok((source.read_override("pack.txt")?, source)));
        let (text, mut source) = match opened {
            Ok((text, source)) => (
                String::from_utf8_lossy(&text.unwrap_or_default()).into_owned(),
                source,
            ),
            Err(error) => {
                tracing::warn!("cannot read texture pack {}: {error}", path.display());
                continue;
            }
        };
        let mut lines = text.lines();
        let description =
            std::array::from_fn(|_| lines.next().unwrap_or("").chars().take(34).collect());
        let thumbnail = source
            .read_override("pack.png")
            .and_then(|bytes| match bytes {
                Some(bytes) => decode("pack.png", &bytes),
                None => builtin(defaults, "gui/unknown_pack.png"),
            })
            .unwrap_or_else(|_| packs[0].thumbnail.clone());
        packs.push(Pack {
            name: name.into(),
            description,
            thumbnail,
        });
    }
    Ok(packs)
}

/// The `.zip` files in `directory`, by name.
fn zips(directory: &Path) -> io::Result<Vec<DirEntry>> {
    let entries = match std::fs::read_dir(directory) {
        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
        entries => entries?,
    };
    let mut zips = Vec::new();
    for entry in entries {
        let entry = entry?;
        if entry
            .path()
            .extension()
            .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
        {
            zips.push(entry);
        }
    }
    zips.sort_by_key(DirEntry::file_name);
    Ok(zips)
}

pub fn stamps(directory: &Path) -> io::Result<Vec<Stamp>> {
    zips(directory)?
        .into_iter()
        .map(|entry| {
            let metadata = entry.metadata()?;
            Ok((entry.file_name(), metadata.len(), metadata.modified().ok()))
        })
        .collect()
}

pub(super) fn read_default(defaults: &Path, name: &str) -> Result<Vec<u8>, AssetError> {
    let path = defaults.join(name);
    std::fs::read(&path).map_err(|source| AssetError::Read { path, source })
}

pub(super) struct Source<'a> {
    defaults: &'a Path,
    archive: Option<zip::ZipArchive<File>>,
}

impl<'a> Source<'a> {
    pub fn open(defaults: &'a Path, pack: Option<&Path>) -> Result<Self, AssetError> {
        let archive = pack
            .map(|path| {
                let file = File::open(path).map_err(|source| AssetError::Read {
                    path: path.into(),
                    source,
                })?;
                zip::ZipArchive::new(file).map_err(|error| AssetError::Pack(error.to_string()))
            })
            .transpose()?;
        Ok(Self { defaults, archive })
    }

    /// The file from the texture pack, if it has one.
    fn read_override(&mut self, name: &str) -> Result<Option<Vec<u8>>, AssetError> {
        let Some(archive) = &mut self.archive else {
            return Ok(None);
        };
        let mut file = match archive.by_name(name) {
            Ok(file) => file,
            Err(zip::result::ZipError::FileNotFound) => return Ok(None),
            Err(error) => return Err(AssetError::Pack(error.to_string())),
        };
        if file.size() > MAX_ASSET_BYTES {
            return Err(AssetError::Pack(format!(
                "Texture pack asset {name} is too large"
            )));
        }
        let mut bytes = Vec::with_capacity(file.size() as usize);
        file.read_to_end(&mut bytes)
            .map_err(|error| AssetError::Pack(error.to_string()))?;
        Ok(Some(bytes))
    }

    pub fn read(&mut self, name: &str) -> Result<Vec<u8>, AssetError> {
        match self.read_override(name)? {
            Some(bytes) => Ok(bytes),
            None => read_default(self.defaults, name),
        }
    }
}