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