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