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 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,
});
}
}
}
}
}