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