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 settings mod.rs
mod keys;
use std::fmt::Write as _;
use std::io::{self, Write};
use std::path::Path;
use std::time::Duration;
pub use keys::{Binding, key_code, key_name};
use server::{ViewArea, ViewShape};
use crate::gpu::{PresentMode, Presentation};
/// The Max FPS slider moves in steps of this many frames per second.
const FPS_STEP: u16 = 10;
/// Max FPS slider positions, the last of which is unlimited.
const FPS_STEPS: u16 = 101;
/// Menus without a world behind them draw at most this many frames per second.
const MENU_FPS: u16 = 60;
/// In chunks.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RenderDistance(pub u8);
impl RenderDistance {
pub const MIN: u8 = 2;
pub const MAX: u8 = 128;
pub fn new(chunks: u8) -> Self {
Self(chunks.clamp(Self::MIN, Self::MAX))
}
pub fn blocks(self) -> f32 {
f32::from(self.0) * 16.0
}
/// From 0 at 2 chunks to 1 at 16 chunks and beyond, how far the fog color reaches.
pub fn fog_factor(self) -> f32 {
((f32::from(self.0).log2() - 1.0) / 3.0).clamp(0.0, 1.0)
}
/// Halves, wrapping from the minimum to the maximum.
pub fn shorter(self) -> Self {
if self.0 == Self::MIN {
Self(Self::MAX)
} else {
Self::new(self.0 / 2)
}
}
/// Doubles, wrapping from the maximum to the minimum.
pub fn farther(self) -> Self {
if self.0 == Self::MAX {
Self(Self::MIN)
} else {
Self::new(self.0.saturating_mul(2))
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Options {
pub music: f32,
pub sound: f32,
pub sensitivity: f32,
pub invert_mouse: bool,
pub render_distance: RenderDistance,
pub render_shape: ViewShape,
pub gui_scale: u8,
pub view_bobbing: bool,
/// `None` is unlimited.
pub max_fps: Option<u16>,
pub present_mode: PresentMode,
pub difficulty: u8,
pub fancy: bool,
pub smooth_lighting: bool,
pub descriptor_heaps: bool,
pub frame_pacing: bool,
pub texture_pack: String,
pub last_server: String,
pub keys: [i32; 10],
extra: Vec<String>,
}
impl Default for Options {
fn default() -> Self {
Self {
music: 1.0,
sound: 1.0,
sensitivity: 0.5,
invert_mouse: false,
render_distance: RenderDistance(12),
render_shape: ViewShape::Square,
gui_scale: 0,
view_bobbing: true,
max_fps: Some(120),
present_mode: PresentMode::Mailbox,
difficulty: 2,
fancy: true,
smooth_lighting: true,
descriptor_heaps: true,
frame_pacing: false,
texture_pack: "Default".into(),
last_server: String::new(),
keys: [17, 30, 31, 32, 57, 42, 16, 18, 20, 33],
extra: Vec::new(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Setting {
Music,
Sound,
InvertMouse,
Sensitivity,
Difficulty,
Graphics,
RenderDistance,
RenderShape,
SmoothLighting,
MaxFps,
ViewBobbing,
GuiScale,
DescriptorHeaps,
PresentMode,
FramePacing,
}
impl Setting {
pub const GENERAL: [Self; 5] = [
Self::Music,
Self::Sound,
Self::InvertMouse,
Self::Sensitivity,
Self::Difficulty,
];
pub const VIDEO: [Self; 10] = [
Self::Graphics,
Self::RenderDistance,
Self::RenderShape,
Self::SmoothLighting,
Self::MaxFps,
Self::ViewBobbing,
Self::GuiScale,
Self::PresentMode,
Self::FramePacing,
Self::DescriptorHeaps,
];
pub fn slider(self, options: &Options) -> Option<f32> {
match self {
Self::Music => Some(options.music),
Self::Sound => Some(options.sound),
Self::Sensitivity => Some(options.sensitivity),
Self::RenderDistance => Some(
f32::from(options.render_distance.0 - RenderDistance::MIN)
/ f32::from(RenderDistance::MAX - RenderDistance::MIN),
),
Self::MaxFps => {
let step = options.max_fps.map_or(FPS_STEPS, |fps| fps / FPS_STEP);
Some(f32::from(step - 1) / f32::from(FPS_STEPS - 1))
}
_ => None,
}
}
pub fn set_slider(self, options: &mut Options, value: f32) {
let value = value.clamp(0.0, 1.0);
match self {
Self::Music => options.music = value,
Self::Sound => options.sound = value,
Self::Sensitivity => options.sensitivity = value,
Self::RenderDistance => {
let range = f32::from(RenderDistance::MAX - RenderDistance::MIN);
options.render_distance =
RenderDistance(RenderDistance::MIN + (value * range).round() as u8);
}
Self::MaxFps => {
let step = 1 + (value * f32::from(FPS_STEPS - 1)).round() as u16;
options.max_fps = (step < FPS_STEPS).then_some(step * FPS_STEP);
}
_ => {}
}
}
/// Moves to the next value; the present mode moves to the next of `present_modes`.
pub fn cycle(self, options: &mut Options, present_modes: &[PresentMode]) {
match self {
Self::InvertMouse => options.invert_mouse = !options.invert_mouse,
Self::Difficulty => options.difficulty = (options.difficulty + 1) % 4,
Self::Graphics => options.fancy = !options.fancy,
Self::RenderShape => {
options.render_shape = match options.render_shape {
ViewShape::Square => ViewShape::Disk,
ViewShape::Disk => ViewShape::Square,
}
}
Self::SmoothLighting => options.smooth_lighting = !options.smooth_lighting,
Self::ViewBobbing => options.view_bobbing = !options.view_bobbing,
Self::GuiScale => options.gui_scale = (options.gui_scale + 1) % 4,
Self::DescriptorHeaps => options.descriptor_heaps = !options.descriptor_heaps,
Self::PresentMode => {
let current = present_modes
.iter()
.position(|&m| m == options.present_mode);
let next = current.map_or(0, |index| (index + 1) % present_modes.len());
if let Some(&mode) = present_modes.get(next) {
options.present_mode = mode;
}
}
Self::FramePacing => options.frame_pacing = !options.frame_pacing,
_ => {}
}
}
pub fn label(self, options: &Options) -> String {
let on = |value| if value { "ON" } else { "OFF" };
let percent = |value: f32, scale: f32| format!("{}%", (value * scale) as u32);
let volume = |value| {
if value == 0.0 {
"OFF".into()
} else {
percent(value, 100.0)
}
};
let (name, value): (_, String) = match self {
Self::Music => ("Music", volume(options.music)),
Self::Sound => ("Sound", volume(options.sound)),
Self::Sensitivity => (
"Sensitivity",
match options.sensitivity {
0.0 => "*yawn*".into(),
1.0 => "HYPERSPEED!!!".into(),
value => percent(value, 200.0),
},
),
Self::InvertMouse => ("Invert Mouse", on(options.invert_mouse).into()),
Self::Difficulty => {
let names = ["Peaceful", "Easy", "Normal", "Hard"];
("Difficulty", names[options.difficulty as usize].into())
}
Self::Graphics => (
"Graphics",
if options.fancy { "Fancy" } else { "Fast" }.into(),
),
Self::RenderDistance => ("Render Distance", options.render_distance.0.to_string()),
Self::RenderShape => (
"Render Shape",
match options.render_shape {
ViewShape::Square => "Quad",
ViewShape::Disk => "Disk",
}
.into(),
),
Self::SmoothLighting => ("Smooth Lighting", on(options.smooth_lighting).into()),
Self::MaxFps => (
"Max FPS",
options
.max_fps
.map_or_else(|| "Unlimited".into(), |fps| fps.to_string()),
),
Self::ViewBobbing => ("View Bobbing", on(options.view_bobbing).into()),
Self::GuiScale => {
let names = ["Auto", "Small", "Normal", "Large"];
("GUI Scale", names[options.gui_scale as usize].into())
}
Self::DescriptorHeaps => (
"Descriptors",
if options.descriptor_heaps {
"Heaps"
} else {
"Sets"
}
.into(),
),
Self::PresentMode => (
"Present Mode",
present_mode_names(options.present_mode).1.into(),
),
Self::FramePacing => {
let pacing = options.presentation().pacing;
("Frame Pacing", on(pacing).into())
}
};
format!("{name}: {value}")
}
}
impl Options {
pub fn load(dir: &Path) -> io::Result<Self> {
match std::fs::read_to_string(dir.join("options.txt")) {
Ok(text) => Ok(Self::parse(&text)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(Self::default()),
Err(error) => Err(error),
}
}
fn parse(text: &str) -> Self {
let mut result = Self::default();
for line in text.lines() {
let Some((key, value)) = line.split_once(':') else {
result.extra.push(line.into());
continue;
};
let float = |target: &mut f32| {
let value = match value {
"true" => Some(1.0),
"false" => Some(0.0),
_ => value.parse::<f32>().ok(),
};
if let Some(value) = value.filter(|v| v.is_finite()) {
*target = value.clamp(0.0, 1.0);
}
};
let number = |target: &mut u8, max| {
if let Ok(value) = value.parse::<u8>() {
*target = value.min(max);
}
};
let boolean = |target: &mut bool| {
if let Ok(value) = value.parse() {
*target = value;
}
};
match key {
"music" => float(&mut result.music),
"sound" => float(&mut result.sound),
"mouseSensitivity" => float(&mut result.sensitivity),
"invertYMouse" => boolean(&mut result.invert_mouse),
"renderDistance" => {
if let Ok(chunks) = value.parse() {
result.render_distance = RenderDistance::new(chunks);
}
}
"renderShape" => match value {
"quad" => result.render_shape = ViewShape::Square,
"disk" => result.render_shape = ViewShape::Disk,
_ => {}
},
// Replaced by renderDistance: Far, Normal, Short and Tiny
"viewDistance" => {
let chunks = [13, 8, 4, 2];
if let Some(&chunks) = value.parse::<usize>().ok().and_then(|i| chunks.get(i)) {
result.render_distance = RenderDistance(chunks);
}
}
"guiScale" => number(&mut result.gui_scale, 3),
"bobView" => boolean(&mut result.view_bobbing),
"anaglyph3d" | "advancedOpengl" => {}
"maxFps" => {
if let Ok(fps) = value.parse::<u16>() {
result.max_fps =
Some(fps.div_ceil(FPS_STEP).clamp(1, FPS_STEPS - 1) * FPS_STEP);
} else if value == "unlimited" {
result.max_fps = None;
}
}
// Replaced by maxFps: Max FPS, Balanced and Power saver
"fpsLimit" => {
let limits = [None, Some(120), Some(40)];
if let Some(&fps) = value.parse::<usize>().ok().and_then(|i| limits.get(i)) {
result.max_fps = fps;
}
}
"presentMode" => {
if let Some(mode) = PresentMode::ALL
.into_iter()
.find(|&mode| present_mode_names(mode).0 == value)
{
result.present_mode = mode;
}
}
"difficulty" => number(&mut result.difficulty, 3),
"fancyGraphics" => boolean(&mut result.fancy),
"ao" => boolean(&mut result.smooth_lighting),
"descriptorHeaps" => boolean(&mut result.descriptor_heaps),
"framePacing" => boolean(&mut result.frame_pacing),
"skin" => result.texture_pack = value.into(),
"lastServer" => result.last_server = value.replace('_', ":"),
_ => {
if let Some(index) = Binding::ALL
.iter()
.position(|binding| key.strip_prefix("key_key.") == Some(binding.name()))
{
if let Ok(value) = value.parse::<i32>() {
result.keys[index] = value;
}
} else {
result.extra.push(line.into());
}
}
}
}
result
}
fn encode(&self) -> String {
let mut text = format!(
"music:{}\nsound:{}\ninvertYMouse:{}\nmouseSensitivity:{}\nrenderDistance:{}\nrenderShape:{}\nguiScale:{}\nbobView:{}\nmaxFps:{}\npresentMode:{}\ndifficulty:{}\nfancyGraphics:{}\nao:{}\ndescriptorHeaps:{}\nframePacing:{}\nskin:{}\nlastServer:{}\n",
self.music,
self.sound,
self.invert_mouse,
self.sensitivity,
self.render_distance.0,
match self.render_shape {
ViewShape::Square => "quad",
ViewShape::Disk => "disk",
},
self.gui_scale,
self.view_bobbing,
self.max_fps
.map_or_else(|| "unlimited".into(), |fps| fps.to_string()),
present_mode_names(self.present_mode).0,
self.difficulty,
self.fancy,
self.smooth_lighting,
self.descriptor_heaps,
self.frame_pacing,
self.texture_pack,
self.last_server.replace(':', "_")
);
for (binding, key) in Binding::ALL.iter().zip(self.keys) {
writeln!(text, "key_key.{}:{key}", binding.name()).unwrap();
}
for line in &self.extra {
writeln!(text, "{line}").unwrap();
}
text
}
pub fn save(&self, dir: &Path) -> io::Result<()> {
std::fs::create_dir_all(dir)?;
let temporary = dir.join("options.txt.tmp");
let mut file = std::fs::File::create(&temporary)?;
file.write_all(self.encode().as_bytes())?;
file.sync_all()?;
std::fs::rename(temporary, dir.join("options.txt"))?;
std::fs::File::open(dir)?.sync_all()
}
pub fn matches(&self, binding: Binding, key: winit::keyboard::KeyCode) -> bool {
key_code(key).is_some_and(|code| self.keys[binding as usize] == code)
}
pub fn frame_interval(&self, menu: bool) -> Duration {
let fps = if menu {
Some(self.max_fps.unwrap_or(MENU_FPS).min(MENU_FPS))
} else {
self.max_fps
};
fps.map_or(Duration::ZERO, |fps| {
Duration::from_secs_f64(1.0 / f64::from(fps))
})
}
/// The client meshes a column once its neighbors arrived, so the server sends one more ring.
pub fn view_area(&self) -> ViewArea {
ViewArea {
radius: i32::from(self.render_distance.0) + 1,
shape: self.render_shape,
}
}
/// Frame pacing applies to FIFO present modes only.
pub fn presentation(&self) -> Presentation {
Presentation {
mode: self.present_mode,
pacing: self.frame_pacing && self.present_mode.is_fifo(),
}
}
}
/// The options file key and button label of a present mode.
fn present_mode_names(mode: PresentMode) -> (&'static str, &'static str) {
match mode {
PresentMode::Immediate => ("immediate", "Immediate"),
PresentMode::Mailbox => ("mailbox", "Mailbox"),
PresentMode::Fifo => ("fifo", "FIFO"),
PresentMode::FifoRelaxed => ("fifoRelaxed", "FIFO Relaxed"),
PresentMode::FifoLatestReady => ("fifoLatestReady", "FIFO Latest"),
}
}