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

mod app;
mod assets;
mod audio;
mod entities;
mod environment;
mod gpu;
mod gui;
mod interaction;
mod menu;
mod particles;
mod player;
mod render;
mod screenshot;
mod settings;
mod stats;
mod timer;
mod weather;
mod world;

use std::path::PathBuf;
use std::process::ExitCode;
use std::time::{SystemTime, UNIX_EPOCH};

use common::JavaRandom;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;

use crate::app::{App, Settings};
use crate::gpu::Validation;
use crate::settings::RenderDistance;

const USAGE: &str = "usage: minecraft [--game-dir <dir>] [--world <name>] [--seed <text>] [--username <name>] [--assets <dir>] [--render-distance <2-128>]";

#[derive(Debug, thiserror::Error)]
enum ArgumentError {
    #[error("{0} needs a value")]
    MissingValue(String),
    #[error("unknown render distance {0:?}")]
    RenderDistance(String),
    #[error("unknown argument {0:?}")]
    Unknown(String),
    #[error("username must contain 1 to 16 letters, digits, or underscores")]
    Username,
}

fn main() -> ExitCode {
    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .init();
    let mut settings = match parse_arguments(std::env::args().skip(1)) {
        Ok(settings) => settings,
        Err(error) => {
            eprintln!("{error}\n{USAGE}");
            return ExitCode::FAILURE;
        }
    };
    settings.validation = match Validation::from_env() {
        Ok(validation) => validation,
        Err(error) => {
            tracing::error!("{error}");
            return ExitCode::FAILURE;
        }
    };
    match App::run(settings) {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            tracing::error!("{error}");
            ExitCode::FAILURE
        }
    }
}

fn parse_arguments(mut arguments: impl Iterator<Item = String>) -> Result<Settings, ArgumentError> {
    let mut settings = Settings {
        assets: PathBuf::from(assets::DEFAULT_DIR),
        // `Minecraft.getMinecraftDir` on Linux
        game_dir: std::env::var_os("HOME")
            .map_or_else(|| PathBuf::from("."), PathBuf::from)
            .join(".minecraft"),
        world: None,
        username: "Player".into(),
        seed: random_seed(),
        render_distance: None,
        validation: None,
    };
    while let Some(argument) = arguments.next() {
        let mut value = || {
            arguments
                .next()
                .ok_or_else(|| ArgumentError::MissingValue(argument.clone()))
        };
        match argument.as_str() {
            "--seed" => settings.seed = parse_seed(&value()?).unwrap_or(settings.seed),
            "--assets" => settings.assets = PathBuf::from(value()?),
            "--game-dir" => settings.game_dir = PathBuf::from(value()?),
            "--world" => settings.world = Some(value()?),
            "--username" => settings.username = value()?,
            "--render-distance" => {
                let distance = value()?;
                let chunks = distance
                    .parse()
                    .ok()
                    .filter(|chunks| (RenderDistance::MIN..=RenderDistance::MAX).contains(chunks))
                    .ok_or(ArgumentError::RenderDistance(distance))?;
                settings.render_distance = Some(RenderDistance(chunks));
            }
            _ => return Err(ArgumentError::Unknown(argument)),
        }
    }
    let valid = |ch: u8| ch.is_ascii_alphanumeric() || ch == b'_';
    if !(1..=16).contains(&settings.username.len()) || !settings.username.bytes().all(valid) {
        return Err(ArgumentError::Username);
    }
    Ok(settings)
}

/// Port of the seed handling in `GuiCreateWorld`: numbers are used as-is, other text gets
/// `String.hashCode`.
fn parse_seed(text: &str) -> Option<i64> {
    match text.parse::<i64>() {
        Ok(seed) => (seed != 0).then_some(seed),
        Err(_) if text.is_empty() => None,
        Err(_) => Some(i64::from(text.encode_utf16().fold(0_i32, |hash, unit| {
            hash.wrapping_mul(31).wrapping_add(i32::from(unit))
        }))),
    }
}

fn random_seed() -> i64 {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |d| d.as_nanos() as i64);
    JavaRandom::new(nanos).next_long()
}