// SPDX-License-Identifier: AGPL-3.0-only //! Main entry point for the Synvael client. //! //! This crate handles window creation, input processing, and drives the renderer to display the game world. mod camera; mod meshing; use std::time::Instant; use anyhow::{Context, Result}; use camera::Camera; use glam::Vec3; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use tracing::{error, info, warn}; use winit::application::ApplicationHandler; use winit::event::{DeviceEvent, DeviceId, ElementState, WindowEvent}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::keyboard::{KeyCode, PhysicalKey}; use winit::window::{CursorGrabMode, Window, WindowId}; /// Transient per-frame input state sampled from window and device events. /// /// Keyboard fields hold whether a movement key is currently pressed. `mouse_delta` accumulates raw pointer motion between frames and is consumed (reset to zero) once applied to the camera. #[expect( clippy::struct_excessive_bools, reason = "per-key held states are independent; a flat bool struct is the clearest representation" )] #[derive(Default)] struct InputState { /// Whether the "move forward" key (W) is held. forward: bool, /// Whether the "move backward" key (S) is held. backward: bool, /// Whether the "strafe left" key (A) is held. left: bool, /// Whether the "strafe right" key (D) is held. right: bool, /// Whether the "move up" key (Space) is held. up: bool, /// Whether the "move down" key (Left Shift) is held. down: bool, /// Accumulated raw mouse motion (x, y) since the last frame, in device units. mouse_delta: (f64, f64), } /// Top-level application state driving the window, renderer, and camera. struct App { /// The Vulkan renderer, initialised once the window exists. renderer: Option, /// The application window, created on resume. window: Option, /// The free-fly camera supplying the view matrix each frame. camera: Camera, /// The current keyboard and mouse input state. input: InputState, /// Timestamp of the previous frame, used to derive delta-time. `None` before the first frame. last_frame: Option, /// Receives the outcome of the background connect and handshake, drained non-blocking from the event loop. `None` before the connection is started and once the outcome has been observed. handshake_rx: Option, } impl Default for App { fn default() -> Self { Self { renderer: None, window: None, // Start above and behind the origin chunk, looking toward -Z and angled downward. camera: Camera::new( Vec3::new(16.0, 40.0, 60.0), -std::f32::consts::FRAC_PI_2, -0.5, ), input: InputState::default(), last_frame: None, handshake_rx: None, } } } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { let attributes = Window::default_attributes().with_title("Synvael"); let window = match event_loop.create_window(attributes) { Ok(w) => w, Err(e) => { error!("Failed to create window: {e}"); event_loop.exit(); return; } }; let display_handle = match event_loop.display_handle() { Ok(h) => h.as_raw(), Err(e) => { error!("Failed to get display handle: {e}"); event_loop.exit(); return; } }; let window_handle = match window.window_handle() { Ok(h) => h.as_raw(), Err(e) => { error!("Failed to get window handle: {e}"); event_loop.exit(); return; } }; let required_extensions = match ash_window::enumerate_required_extensions(display_handle) { Ok(exts) => exts, Err(e) => { error!("Failed to enumerate required extensions: {e}"); event_loop.exit(); return; } }; let size = window.inner_size(); let renderer = match renderer::Renderer::new( display_handle, window_handle, size.width, size.height, required_extensions, ) { Ok(r) => r, Err(e) => { error!("Failed to initialize Vulkan renderer: {e}"); event_loop.exit(); return; } }; // Confine and hide the pointer so mouse motion drives the camera rather than moving a visible cursor. `Locked` is preferred; some platforms only support `Confined`. if let Err(e) = window .set_cursor_grab(CursorGrabMode::Locked) .or_else(|_| window.set_cursor_grab(CursorGrabMode::Confined)) { warn!("Failed to grab cursor: {e}"); } window.set_cursor_visible(false); self.window = Some(window); self.renderer = Some(renderer); #[expect( clippy::expect_used, reason = "startup asset load; a missing worldgen config is unrecoverable at launch" )] let config_str = std::fs::read_to_string("assets/data/worldgen/default.json") .expect("Failed to read worldgen config"); #[expect( clippy::expect_used, reason = "startup config parse; a malformed worldgen config is unrecoverable at launch" )] let worldgen_config: shared::generator::WorldGenConfig = serde_json::from_str(&config_str).expect("Failed to parse worldgen config"); let seed = 4_813_530; let generator = shared::generator::VoxelGenerator::new(worldgen_config, seed); let chunk = generator.generate_chunk(shared::world::ChunkPos::new(0, 0, 0)); let (vertices, indices) = meshing::generate_mesh(&chunk); tracing::info!( "Generated Mesh with {} vertices and {} indices!", vertices.len(), indices.len() ); #[expect( clippy::expect_used, reason = "the renderer is assigned earlier in this function" )] self.renderer .as_mut() .expect("Renderer initialized") .insert_mesh((0, 0, 0), &vertices, &indices, [0.0, 0.0, 0.0]) .expect("Failed to upload terrain to GPU"); // Kick off a background connect + handshake to the local server. let hello = shared::protocol::ClientHello { protocol_version: shared::protocol::PROTOCOL_VERSION, client_build: env!("CARGO_PKG_VERSION").to_owned(), player_identity: shared::protocol::PlayerIdentity { display_name: "Player".to_owned(), }, installed_packs: Vec::new(), requested_features: shared::protocol::FeatureFlags(0), }; let server_addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, net::DEFAULT_PORT)); info!("Connecting to server at {server_addr}"); self.handshake_rx = Some(net::connect_in_background(server_addr, hello)); } fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { match event { WindowEvent::CloseRequested => { event_loop.exit(); } WindowEvent::KeyboardInput { event, .. } => { let pressed = event.state == ElementState::Pressed; if let PhysicalKey::Code(code) = event.physical_key { match code { KeyCode::KeyW => self.input.forward = pressed, KeyCode::KeyS => self.input.backward = pressed, KeyCode::KeyA => self.input.left = pressed, KeyCode::KeyD => self.input.right = pressed, KeyCode::Space => self.input.up = pressed, KeyCode::ShiftLeft => self.input.down = pressed, KeyCode::Escape => event_loop.exit(), _ => {} } } } WindowEvent::RedrawRequested => { // Non-blocking check for the handshake outcome. let mut handshake_done = false; if let Some(rx) = self.handshake_rx.as_ref() { match rx.try_recv() { Ok(Ok(ack)) => { info!( protocol_version = ack.protocol_version, "handshake complete" ); handshake_done = true; } Ok(Err(reason)) => { warn!("handshake failed: {reason}"); handshake_done = true; } // Empty: not ready yet. Disconnected: the network thread ended. Err(_) => {} } } if handshake_done { self.handshake_rx = None; } // Derive delta-time from the previous frame so movement is framerate-independent. The first frame has no predecessor and therefore advances by zero seconds. let now = Instant::now(); let dt = self .last_frame .map_or(0.0, |prev| now.duration_since(prev).as_secs_f32()); self.last_frame = Some(now); self.camera.update(&self.input, dt); // The accumulated motion has been applied; clear it so it is not counted twice. self.input.mouse_delta = (0.0, 0.0); let view = self.camera.view_matrix(); if let Some(Err(e)) = self.renderer.as_mut().map(|r| r.draw_frame(view)) { error!("Failed to draw frame: {e}"); event_loop.exit(); } if let Some(window) = self.window.as_ref() { window.request_redraw(); } } _ => (), } } fn device_event( &mut self, _event_loop: &ActiveEventLoop, _device_id: DeviceId, event: DeviceEvent, ) { // Raw mouse motion is used for look control; it is unaffected by pointer acceleration or the desktop cursor position, which absolute window coordinates would not guarantee. if let DeviceEvent::MouseMotion { delta } = event { self.input.mouse_delta.0 += delta.0; self.input.mouse_delta.1 += delta.1; } } } fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .init(); info!("Starting Synvael client"); let event_loop = EventLoop::new().context("Failed to create event loop")?; event_loop.set_control_flow(ControlFlow::Poll); let mut app = App::default(); event_loop .run_app(&mut app) .context("Failed to run event loop")?; Ok(()) }