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 / app / navigation.rs

use std::path::PathBuf;
use std::time::Instant;

use super::session::{Backend, Session};
use super::{Game, StartError, remote};
use crate::assets::{Assets, packs};
use crate::menu::{Action, Page};
use common::protocol::{Connection, Packet};
use server::save::catalog;

impl Game {
    pub(super) fn open(&mut self, page: Page) {
        self.menu.open(page, &self.options);
        if page == Page::Worlds {
            match catalog::list(&self.settings.game_dir.join("saves")) {
                Ok(worlds) => {
                    self.menu.worlds = worlds;
                    self.menu.selected = None;
                }
                Err(error) => self.fail("Failed to load worlds", error, Page::Title),
            }
        }
        if page == Page::Packs
            && let Err(error) = self.refresh_packs(true)
        {
            self.fail("Failed to load texture packs", error, Page::Title);
        }
        self.update_folder();
        self.focus();
    }

    pub(super) fn update_folder(&mut self) {
        if self.menu.page == Some(Page::Create) {
            match catalog::unused_folder(
                &self.settings.game_dir.join("saves"),
                &self.menu.fields[0].text,
            ) {
                Ok(folder) => self.menu.folder = folder,
                Err(error) => self.fail("Failed to create world", error, Page::Worlds),
            }
        }
    }

    pub(super) fn fail(&mut self, title: &str, error: impl ToString, parent: Page) {
        tracing::warn!("{title}: {}", error.to_string());
        self.menu.fail(title, error, parent, &self.options);
        self.focus();
    }

    pub(super) fn apply_options(&mut self, save: bool) -> bool {
        let options = &self.options;
        self.audio.set_volume(options.music, options.sound);
        let rendered = self.view.renderer.set_options(
            options.fancy,
            options.smooth_lighting,
            options.descriptor_heaps,
            options.presentation(),
        );
        if let Some(session) = &mut self.session {
            session.backend.options(options);
            session.render_distance = options.render_distance;
            session.player.view_bobbing = options.view_bobbing;
            session.fancy = options.fancy;
        }
        if let Err(error) = rendered {
            self.options.descriptor_heaps = self.view.renderer.descriptor_heaps();
            self.fail(
                "Failed to apply video settings",
                error,
                self.menu.options_parent,
            );
            return false;
        }
        if save && let Err(error) = self.options.save(&self.settings.game_dir) {
            self.fail("Failed to save options", error, self.menu.options_parent);
            return false;
        }
        true
    }

    /// Lists the texture packs again if forced or if the folder changed.
    pub(super) fn refresh_packs(&mut self, force: bool) -> Result<(), StartError> {
        let directory = self.settings.game_dir.join("texturepacks");
        self.packs_checked = Instant::now();
        let stamps = packs::stamps(&directory)?;
        if !force && stamps == self.pack_stamps {
            return Ok(());
        }
        self.menu.packs = packs::list(&directory, &self.settings.assets)?;
        // `TexturePackList.updateAvaliableTexturePacks` falls back to Default for a removed pack
        let removed = self
            .menu
            .packs
            .iter()
            .all(|pack| pack.name != self.options.texture_pack);
        if removed {
            self.choose_pack(0)?;
        } else {
            self.assets.menu.truncate(3);
            self.assets
                .menu
                .extend(self.menu.packs.iter().map(|pack| pack.thumbnail.clone()));
            self.view.renderer.reload_assets(&self.assets)?;
        }
        self.pack_stamps = stamps;
        Ok(())
    }

    pub(super) fn choose_pack(&mut self, index: usize) -> Result<(), StartError> {
        let Some(pack) = self.menu.packs.get(index) else {
            return Ok(());
        };
        let name = pack.name.clone();
        if name == self.options.texture_pack {
            return Ok(());
        }
        let path = packs::path(&self.settings.game_dir.join("texturepacks"), &name)?;
        let mut assets = Assets::load(&self.settings.assets, path.as_deref())?;
        assets
            .menu
            .extend(self.menu.packs.iter().map(|pack| pack.thumbnail.clone()));
        self.view.renderer.reload_assets(&assets)?;
        self.view.gui = crate::gui::Geometry::new(&assets.gui[3], assets.translations.clone());
        self.assets = assets;
        self.options.texture_pack = name;
        self.apply_options(true);
        Ok(())
    }

    pub(super) fn start_world(&mut self, path: PathBuf, name: &str, seed: i64) {
        let existing = path.join("level.dat").exists() || path.join("level.dat_old").exists();
        match server::IntegratedServer::start(server::Settings {
            world: path,
            name: name.into(),
            seed,
            view: self.options.view_area(),
        }) {
            Ok((server, connection)) => {
                self.view.renderer.clear_world();
                let title = if existing {
                    "Loading level"
                } else {
                    "Generating level"
                };
                self.enter(Backend::Local(server), connection, seed, title);
                self.menu.message = "Building terrain".into();
                self.menu.progress = 0.0;
                let mut stats = self.statistics.stats.lock().unwrap();
                stats.add(if existing { 1002 } else { 1001 }, 1);
                stats.add(1000, 1);
            }
            Err(error) => self.fail("Failed to load world", error, Page::Worlds),
        }
    }

    /// Starts a session and shows its loading screen.
    fn enter(&mut self, backend: Backend, connection: Connection, seed: i64, title: &'static str) {
        self.menu.remote = !backend.local();
        self.session = Some(Session::new(
            backend,
            connection,
            seed,
            &self.assets,
            &self.options,
            self.statistics.stats.clone(),
        ));
        self.menu.in_world = true;
        self.open(Page::Loading);
        self.menu.loading_title = title;
    }

    pub(super) fn leave(&mut self) {
        self.set_grab(false);
        if let Some(mut session) = self.session.take() {
            self.audio.record(None, glam::DVec3::ZERO);
            self.statistics.stats.lock().unwrap().add(1004, 1);
            self.statistics.save();
            session.connection.send(Packet::Disconnect {
                reason: "Quitting".into(),
            });
            self.view.renderer.clear_world();
            self.menu.in_world = false;
            if session.backend.local() {
                self.open(Page::Saving);
                self.menu.message = "Saving chunks".into();
                match std::thread::Builder::new()
                    .name("save-world".into())
                    .spawn(move || session.backend.shutdown())
                {
                    Ok(thread) => self.saving = Some(thread),
                    Err(error) => self.fail("Failed to save world", error, Page::Title),
                }
                return;
            }
        }
        self.menu.choose_splash();
        self.open(Page::Title);
        self.quit |= self.quit_after_save;
    }

    pub(super) fn request_quit(&mut self) {
        self.apply_options(true);
        self.quit_after_save = true;
        if self.saving.is_none() {
            self.leave();
        }
    }

    pub(super) fn action(&mut self, action: Action) {
        if self.saving.is_some() && action != Action::Quit {
            return;
        }
        let saves = self.settings.game_dir.join("saves");
        match action {
            Action::StatsTab(tab) => {
                self.menu.stats_tab = tab;
                self.menu.stats_sort = None;
                self.menu.scroll = 0.0;
            }
            Action::StatsSort(column) => {
                self.menu.stats_sort = match self.menu.stats_sort {
                    Some((old, true)) if old == column => Some((column, false)),
                    Some((old, false)) if old == column => None,
                    _ => Some((column, true)),
                };
            }
            Action::Open(page) => self.open(page),
            Action::Back => {
                let back = self.menu.back();
                if matches!(
                    self.menu.page,
                    Some(Page::Options | Page::Video | Page::Controls)
                ) && !self.apply_options(true)
                {
                    return;
                }
                self.action(back);
            }
            Action::Quit => self.request_quit(),
            Action::Resume => {
                self.menu.page = None;
                if let Some(session) = &mut self.session {
                    session.interaction.screen = None;
                }
                self.focus();
                self.set_grab(true);
            }
            Action::Leave => self.leave(),
            Action::Play => {
                if let Some(world) = self.menu.selected_world() {
                    if let Some(problem) = &world.problem {
                        self.fail("Failed to load world", problem.clone(), Page::Worlds);
                        return;
                    }
                    let name = world.name.clone();
                    match catalog::world_path(&saves, &world.folder) {
                        Ok(path) => self.start_world(path, &name, crate::random_seed()),
                        Err(error) => self.fail("Failed to load world", error, Page::Worlds),
                    }
                }
            }
            Action::Create => {
                // Only the folder is trimmed; the level keeps the name as entered
                let name = self.menu.fields[0].text.clone();
                if name.trim().is_empty() {
                    return;
                }
                let seed =
                    crate::parse_seed(&self.menu.fields[1].text).unwrap_or_else(crate::random_seed);
                match catalog::create(&saves, name.trim()) {
                    Ok((_, path)) => self.start_world(path, &name, seed),
                    Err(error) => self.fail("Failed to create world", error, Page::Worlds),
                }
            }
            Action::Rename | Action::Delete => {
                if let Some(world) = self.menu.selected_world() {
                    let result = if action == Action::Rename {
                        catalog::rename(&saves, &world.folder, &self.menu.fields[0].text)
                    } else {
                        catalog::delete(&saves, &world.folder)
                    };
                    match result {
                        Ok(()) => self.open(Page::Worlds),
                        Err(error) => self.fail("World operation failed", error, Page::Worlds),
                    }
                }
            }
            Action::Setting(setting) => {
                if setting.slider(&self.options).is_none() {
                    setting.cycle(&mut self.options, &self.menu.present_modes);
                }
                self.apply_options(setting.slider(&self.options).is_none());
            }
            Action::Bind(binding) => self.menu.binding = Some(binding),
            Action::Connect => {
                let address = self.menu.fields[0].text.trim().to_owned();
                self.options.last_server.clone_from(&address);
                self.apply_options(true);
                match remote::Remote::connect(&address, &self.settings.username) {
                    Ok((remote, connection)) => {
                        let title = "Connecting to the server...";
                        self.enter(Backend::Remote(remote), connection, 0, title);
                        self.menu.message.clear();
                        self.statistics.stats.lock().unwrap().add(1003, 1);
                    }
                    Err(error) => self.fail("Failed to connect to the server", error, Page::Title),
                }
            }
            Action::Pack(index) => {
                if let Err(error) = self.choose_pack(index) {
                    self.fail("Failed to load texture pack", error, Page::Packs);
                }
            }
            Action::OpenPackFolder => {
                let path = self.settings.game_dir.join("texturepacks");
                match std::fs::create_dir_all(&path)
                    .and_then(|()| std::process::Command::new("xdg-open").arg(&path).spawn())
                {
                    // A thread reaps the child
                    Ok(mut child) => drop(std::thread::spawn(move || child.wait())),
                    Err(error) => {
                        self.fail("Failed to open texture pack folder", error, Page::Packs)
                    }
                }
            }
            Action::FinishSign => {
                if let Some(editor) = self.menu.sign.take()
                    && let Some(session) = &mut self.session
                {
                    let [x, y, z] = editor.sign.position;
                    session.world.set_tile_entity(
                        x,
                        y,
                        z,
                        common::world::TileEntity::Sign(editor.lines.clone()),
                    );
                    session.connection.send(Packet::UpdateSign {
                        x,
                        y: y as i16,
                        z,
                        lines: editor.lines,
                    });
                }
                self.action(Action::Resume);
            }
            Action::SendChat => {
                let text = self.menu.fields[0].text.trim();
                if !text.is_empty()
                    && let Some(session) = &mut self.session
                {
                    session.connection.send(Packet::Chat {
                        message: text.into(),
                    });
                }
                if self.menu.page == Some(Page::Sleep) {
                    self.menu.fields[0] = crate::menu::TextField::chat(100);
                } else {
                    self.action(Action::Resume);
                }
            }
            Action::LeaveBed => {
                if let Some(session) = &self.session {
                    session.connection.send(Packet::EntityAction {
                        entity: session.player.entity.id,
                        action: 3,
                    });
                }
            }
        }
    }
}