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 / mod.rs

mod input;
mod navigation;
mod remote;
mod session;
mod warp;

use std::path::PathBuf;
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};

use common::world::shape::BlockView;
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use server::save::SaveError;
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalSize, PhysicalSize};
use winit::event::{DeviceEvent, DeviceId, Ime, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{ModifiersState, PhysicalKey};
use winit::window::{Window, WindowId};

use crate::assets::{Assets, packs};
use crate::gpu::vk::VkExtent2D;
use crate::menu::{Menu, Page};
use crate::render::{FrameParams, Renderer};
use crate::settings::{Options, RenderDistance};
use session::Session;

pub struct Settings {
    pub assets: PathBuf,
    pub game_dir: PathBuf,
    pub world: Option<String>,
    pub seed: i64,
    pub render_distance: Option<RenderDistance>,
    pub username: String,
    pub validation: Option<crate::gpu::Validation>,
}

#[derive(Debug, thiserror::Error)]
pub enum StartError {
    #[error(transparent)]
    Assets(#[from] crate::assets::AssetError),
    #[error("cannot create the window: {0}")]
    Window(#[from] winit::error::OsError),
    #[error("cannot access the window handles: {0}")]
    Handle(#[from] raw_window_handle::HandleError),
    #[error("cannot initialize Vulkan: {0}")]
    Gpu(#[from] crate::gpu::Error),
    #[error("cannot start the integrated server: {0}")]
    Server(#[from] server::StartError),
    #[error(transparent)]
    Save(#[from] SaveError),
    #[error("{0}")]
    Io(#[from] std::io::Error),
    #[error("{0}")]
    Disconnected(String),
    #[error("cannot create the event loop: {0}")]
    EventLoop(winit::error::EventLoopError),
    #[error("event loop failed: {0}")]
    Run(winit::error::EventLoopError),
}

pub struct App {
    settings: Option<Settings>,
    game: Option<Game>,
    error: Option<StartError>,
}

impl App {
    pub fn run(settings: Settings) -> Result<(), StartError> {
        let event_loop = EventLoop::new().map_err(StartError::EventLoop)?;
        let mut app = Self {
            settings: Some(settings),
            game: None,
            error: None,
        };
        event_loop.run_app(&mut app).map_err(StartError::Run)?;
        app.error.map_or(Ok(()), Err)
    }
}

struct View {
    renderer: Renderer,
    gui: crate::gui::Geometry,
    window: Arc<Window>,
    cursor: [f32; 2],
    grabbed: bool,
    pointer_warp: Option<warp::PointerWarp>,
}

impl View {
    /// The GUI size and the cursor position in GUI pixels.
    fn gui_space(&self, gui_scale: u8) -> ([f32; 2], [f32; 2]) {
        let size = self.window.inner_size();
        let (size, scale) = crate::gui::scaled_size([size.width, size.height], gui_scale);
        (size, self.cursor.map(|v| (v / scale).floor()))
    }
}

fn extent(size: PhysicalSize<u32>) -> VkExtent2D {
    VkExtent2D {
        width: size.width,
        height: size.height,
    }
}

fn join_save(thread: JoinHandle<Result<(), SaveError>>) -> Result<(), SaveError> {
    let panicked = |_| Err(SaveError::Invalid("World save thread panicked".into()));
    thread.join().unwrap_or_else(panicked)
}

struct Game {
    view: View,
    assets: Assets,
    settings: Settings,
    options: Options,
    menu: Menu,
    session: Option<Session>,
    saving: Option<JoinHandle<Result<(), SaveError>>>,
    screenshots: crate::screenshot::Screenshots,
    statistics: crate::stats::Store,
    statistics_saved: Instant,
    quit: bool,
    quit_after_save: bool,
    audio: crate::audio::Audio,
    modifiers: ModifiersState,
    /// The held left and right Shift keys.
    shift_keys: [bool; 2],
    next_frame: Instant,
    focused: bool,
    minimized: bool,
    clipboard: Option<arboard::Clipboard>,
    ime_enabled: bool,
    pack_stamps: Vec<packs::Stamp>,
    packs_checked: Instant,
}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        let Some(settings) = self.settings.take() else {
            return;
        };
        match Game::start(event_loop, settings) {
            Ok(game) => self.game = Some(game),
            Err(error) => {
                self.error = Some(error);
                event_loop.exit();
            }
        }
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _window: WindowId,
        event: WindowEvent,
    ) {
        let Some(game) = &mut self.game else { return };
        match event {
            WindowEvent::CloseRequested => game.request_quit(),
            WindowEvent::Resized(size) => {
                game.minimized = size.width == 0 || size.height == 0;
                game.view.renderer.resize(extent(size));
                let max = game.menu.max_scroll(game.gui_space().0);
                game.menu.scroll = game.menu.scroll.min(max);
            }
            WindowEvent::Focused(focused) => {
                game.focused = focused;
                if !focused {
                    game.shift_keys = [false; 2];
                    if let Some(session) = &mut game.session {
                        session.interaction.shift = false;
                        if game.menu.page.is_none() && session.interaction.screen.is_none() {
                            game.open(Page::Pause);
                        }
                    }
                    game.set_grab(false);
                }
            }
            WindowEvent::ModifiersChanged(modifiers) => game.modifiers = modifiers.state(),
            WindowEvent::CursorMoved { position, .. } => {
                game.view.cursor = [position.x as f32, position.y as f32];
                if let Some(session) = &mut game.session {
                    session.interaction.cursor = game.view.cursor;
                }
                let (size, cursor) = game.gui_space();
                if game.menu.mouse_move(cursor, size, &mut game.options) {
                    game.apply_options(false);
                }
            }
            WindowEvent::MouseInput { state, button, .. } => game.mouse(button, state.is_pressed()),
            WindowEvent::MouseWheel { delta, .. } => {
                let y = match delta {
                    MouseScrollDelta::LineDelta(_, y) => y,
                    MouseScrollDelta::PixelDelta(pos) => pos.y as f32 / 18.0,
                };
                if game.menu.page.is_some() {
                    game.menu.scroll(y, game.gui_space().0);
                } else if game.view.grabbed
                    && y != 0.0
                    && let Some(session) = &mut game.session
                {
                    let slot = (session.interaction.inventory.selected as i32 - y.signum() as i32)
                        .rem_euclid(9) as usize;
                    session.interaction.select(slot, &session.connection);
                }
            }
            WindowEvent::KeyboardInput { event, .. } => {
                if let PhysicalKey::Code(code) = event.physical_key {
                    let text = event.text.as_deref();
                    game.key(code, event.state.is_pressed(), event.repeat, text);
                }
            }
            WindowEvent::Ime(Ime::Commit(text)) => {
                for field in game.menu.fields.iter_mut().filter(|field| field.focused) {
                    field.insert(&text);
                }
                game.update_folder();
            }
            WindowEvent::RedrawRequested => {
                let frame_start = Instant::now();
                if let Err(error) = game.frame() {
                    self.error = Some(error);
                    game.quit = true;
                }
                game.next_frame = game.view.renderer.next_frame().unwrap_or_else(|| {
                    frame_start + game.options.frame_interval(game.session.is_none())
                });
            }
            _ => {}
        }
        if game.quit {
            event_loop.exit();
        }
    }

    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
        // The window's Wayland and Vulkan objects must go before winit disconnects the display
        self.game = None;
    }

    fn device_event(
        &mut self,
        _event_loop: &ActiveEventLoop,
        _device: DeviceId,
        event: DeviceEvent,
    ) {
        if let (Some(game), DeviceEvent::MouseMotion { delta: (dx, dy) }) = (&mut self.game, event)
            && game.view.grabbed
            && let Some(session) = &mut game.session
        {
            let factor = (game.options.sensitivity * 0.6 + 0.2).powi(3) * 8.0;
            let invert = if game.options.invert_mouse { -1.0 } else { 1.0 };
            let turn = [dx as f32 * factor, -dy as f32 * factor * invert];
            if session.smooth_camera {
                session.camera_turn[0] += turn[0];
                session.camera_turn[1] += turn[1];
            } else {
                session.player.entity.turn(turn[0], turn[1]);
            }
        }
    }

    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
        let Some(game) = &mut self.game else { return };
        if game.minimized && Instant::now() >= game.next_frame {
            if let Err(error) = game.frame() {
                self.error = Some(error);
                game.quit = true;
            }
            game.next_frame = Instant::now() + Duration::from_millis(50);
        }
        if game.quit {
            event_loop.exit();
            return;
        }
        if Instant::now() >= game.next_frame {
            game.view.window.request_redraw();
        }
        event_loop.set_control_flow(ControlFlow::WaitUntil(game.next_frame));
    }
}

impl Game {
    fn start(event_loop: &ActiveEventLoop, settings: Settings) -> Result<Self, StartError> {
        let mut options = Options::load(&settings.game_dir)?;
        if let Some(distance) = settings.render_distance {
            options.render_distance = distance;
        }
        let selected_pack = packs::path(
            &settings.game_dir.join("texturepacks"),
            &options.texture_pack,
        );
        let (assets, pack_error) =
            match selected_pack.and_then(|pack| Assets::load(&settings.assets, pack.as_deref())) {
                Ok(assets) => (assets, None),
                Err(error) => {
                    options.texture_pack = "Default".into();
                    (Assets::load(&settings.assets, None)?, Some(error))
                }
            };
        let window = Arc::new(
            event_loop.create_window(
                Window::default_attributes()
                    .with_title("Minecraft")
                    .with_inner_size(LogicalSize::new(854, 480))
                    .with_min_inner_size(LogicalSize::new(320, 240)),
            )?,
        );
        let pointer_warp = warp::PointerWarp::new(&window).unwrap_or_else(|error| {
            tracing::warn!("cannot set up pointer warping: {error}");
            None
        });
        let renderer = unsafe {
            Renderer::new(
                window.display_handle()?.as_raw(),
                window.window_handle()?.as_raw(),
                extent(window.inner_size()),
                &assets,
                settings.validation,
                options.descriptor_heaps,
                options.presentation(),
            )?
        };
        tracing::info!("rendering with {}", renderer.device_name());
        options.descriptor_heaps = renderer.descriptor_heaps();
        options.present_mode = renderer.presentation().mode;
        options.frame_pacing &= renderer.supports_frame_pacing();
        let gui = crate::gui::Geometry::new(&assets.gui[3], assets.translations.clone());
        let splashes =
            std::fs::read_to_string(settings.assets.join("title/splashes.txt")).unwrap_or_default();
        let statistics =
            crate::stats::Store::open(&settings.game_dir, &settings.assets, &settings.username)?;
        let mut menu = Menu::new(&splashes, settings.seed);
        menu.statistics = statistics.stats.clone();
        menu.descriptor_heaps = renderer.supports_descriptor_heaps();
        menu.frame_pacing = renderer.supports_frame_pacing();
        menu.present_modes = renderer.present_modes().to_vec();
        let mut game = Self {
            view: View {
                renderer,
                gui,
                window,
                cursor: [0.0; 2],
                grabbed: false,
                pointer_warp,
            },
            assets,
            options,
            menu,
            statistics,
            statistics_saved: Instant::now(),
            session: None,
            saving: None,
            screenshots: crate::screenshot::Screenshots::new(&settings.game_dir)?,
            quit: false,
            quit_after_save: false,
            audio: crate::audio::Audio::new(&settings.assets, settings.seed),
            modifiers: ModifiersState::empty(),
            shift_keys: [false; 2],
            next_frame: Instant::now(),
            focused: true,
            minimized: false,
            clipboard: arboard::Clipboard::new().ok(),
            ime_enabled: false,
            pack_stamps: Vec::new(),
            packs_checked: Instant::now(),
            settings,
        };
        game.apply_options(false);
        if let Some(error) = pack_error {
            game.fail("Failed to load texture pack", error, Page::Title);
        }
        if let Some(world) = game.settings.world.clone() {
            let path =
                server::save::catalog::world_path(&game.settings.game_dir.join("saves"), &world)?;
            game.start_world(path, &world, game.settings.seed);
        }
        Ok(game)
    }

    fn gui_space(&self) -> ([f32; 2], [f32; 2]) {
        self.view.gui_space(self.options.gui_scale)
    }

    fn frame(&mut self) -> Result<(), StartError> {
        self.screenshots
            .submit(self.view.renderer.take_screenshots());
        for message in self.screenshots.messages() {
            if let Some(session) = &mut self.session {
                session.message(message);
            }
        }
        if self.menu.page == Some(Page::Packs)
            && self.packs_checked.elapsed().as_secs() >= 1
            && let Err(error) = self.refresh_packs(false)
        {
            self.fail("Failed to load texture packs", error, Page::Title);
        }
        if let Some(thread) = self.saving.take_if(|thread| thread.is_finished()) {
            match join_save(thread) {
                Ok(()) => {
                    if self.menu.page != Some(Page::Error) {
                        self.open(Page::Title);
                    }
                    self.quit |= self.quit_after_save;
                }
                Err(error) => {
                    self.quit_after_save = false;
                    self.fail("Failed to save world", error, Page::Title);
                }
            }
        }
        let (position, yaw) = self
            .session
            .as_ref()
            .map_or((glam::DVec3::ZERO, 0.0), Session::listener);
        self.audio.tick(position, yaw);
        if let Some(session) = &mut self.session {
            if let Err(error) = session.receive(&mut self.view.renderer, &self.audio) {
                let title = match (self.menu.page, session.backend.local()) {
                    (Some(Page::Loading), true) => "Failed to load world",
                    (Some(Page::Loading), false) => "Failed to connect to the server",
                    _ => "Connection Lost",
                };
                self.leave();
                self.fail(title, error, Page::Title);
            } else {
                if let Some(sign) = session.interaction.placed_sign.take_if(|sign| {
                    let [x, y, z] = sign.position;
                    matches!(
                        session.world.block(x, y, z),
                        common::world::Block::SIGN | common::world::Block::WALL_SIGN
                    )
                }) {
                    self.menu.open(Page::Sign, &self.options);
                    self.menu.sign = Some(crate::menu::SignEditor {
                        sign,
                        lines: Default::default(),
                        line: 0,
                    });
                }
                if let Some(dimension) = session.travelled.take() {
                    self.menu.page = None;
                    self.menu.open(Page::Loading, &self.options);
                    self.menu.message = "Building terrain".into();
                    self.menu.loading_title = if dimension == -1 {
                        "Entering the Nether"
                    } else {
                        "Leaving the Nether"
                    };
                }
                self.menu.progress = session.loading_progress();
                if self.menu.page == Some(Page::Loading) {
                    const CONVERTING: &str = "Converting World to Scaevolus' McRegion";
                    match &session.backend {
                        session::Backend::Local(server) => {
                            if let Some(percent) = server.conversion() {
                                self.menu.loading_title = CONVERTING;
                                self.menu.message = "This may take a while :)".into();
                                self.menu.progress = percent as f32 / 100.0;
                            } else if self.menu.loading_title == CONVERTING {
                                self.menu.loading_title = "Loading level";
                                self.menu.message = "Building terrain".into();
                            }
                        }
                        session::Backend::Remote(remote) => {
                            self.menu.loading_title = if session.logged_in {
                                "Downloading terrain"
                            } else if remote.connected() {
                                "Logging in..."
                            } else {
                                "Connecting to the server..."
                            };
                        }
                    }
                    if session.ready() && self.view.renderer.column_ready(session.center()) {
                        self.menu.page = None;
                        self.set_grab(true);
                    }
                }
                self.focus();
            }
        }
        if let Some(session) = &self.session {
            let entity = &session.player.entity;
            let sleeping = entity.sleep.sleeping;
            let alive = entity.living.as_ref().is_some_and(|l| l.health > 0);
            if !session.backend.local()
                && sleeping
                && alive
                && self.menu.page.is_none()
                && session.interaction.screen.is_none()
            {
                self.open(Page::Sleep);
            } else if self.menu.page == Some(Page::Sleep) && !sleeping {
                self.action(crate::menu::Action::Resume);
            }
        }
        if self.statistics_saved.elapsed() >= Duration::from_secs(5) {
            self.statistics.save();
            self.statistics_saved = Instant::now();
        }
        let interval = self.options.frame_interval(self.session.is_none());
        let now = self
            .view
            .renderer
            .begin_frame(interval)
            .unwrap_or_else(Instant::now);
        if let Some(session) = &mut self.session {
            session.advance(now, &self.audio, &self.menu);
        }
        if self.minimized {
            return Ok(());
        }
        if let Some(session) = &mut self.session {
            session.frame(&mut self.view, &self.menu, &self.options)?;
        } else {
            let size = self.view.window.inner_size();
            self.view
                .gui
                .begin([size.width, size.height], self.options.gui_scale);
            let cursor = self.gui_space().1;
            self.view.gui.menu(&self.menu, &self.options, cursor);
            self.view.renderer.render(
                None,
                &FrameParams {
                    eye: glam::DVec3::ZERO,
                    view: glam::Mat4::IDENTITY,
                    fov_y_degrees: 70.0,
                    view_distance: 256.0,
                    fog_density: None,
                    fog_color: glam::Vec3::ZERO,
                    sky_color: glam::Vec3::ZERO,
                    rain: 0.0,
                    celestial_angle: 0.0,
                    sunrise: None,
                    sky_darkness: 0,
                    has_sky: true,
                    brightness: common::world::light::brightness_table(true),
                    tile_updates: &[],
                    gui: &self.view.gui,
                    scene: &Default::default(),
                },
            )?;
        }
        Ok(())
    }
}

impl Drop for Game {
    fn drop(&mut self) {
        if let Err(error) = self.view.renderer.finish_screenshots() {
            tracing::error!("cannot finish screenshots: {error}");
        }
        self.screenshots
            .submit_on_shutdown(self.view.renderer.take_screenshots());
        if let Some(thread) = self.saving.take()
            && let Err(error) = join_save(thread)
        {
            tracing::error!("world save failed: {error}");
        }
    }
}