library

Online co-op book organization video game
git clone https://git.bradleygannon.com/library.git
Log | Files | Refs

commit 4fb6e412febcc990e8d45be75c17ecec8fe40cb8
parent 1fb690aedead6e7a503c75f58407af5ccb62475a
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Sun, 30 Aug 2026 21:17:32 -0400

Split some files

Diffstat:
MCargo.lock | 1+
Mclient/Cargo.toml | 1+
Dclient/src/game.rs | 32--------------------------------
Aclient/src/game/mod.rs | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aclient/src/game/run.rs | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aclient/src/game/state.rs | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mclient/src/main.rs | 295+++----------------------------------------------------------------------------
Mclient/src/render.rs | 21++++++++++-----------
Dcommon/src/game.rs | 512-------------------------------------------------------------------------------
Acommon/src/game/book.rs | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/src/game/mod.rs | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/src/game/state.rs | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mserver/src/main.rs | 5++++-
13 files changed, 886 insertions(+), 842 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -63,6 +63,7 @@ dependencies = [ "anyhow", "common", "confy", + "lazy_static", "macroquad", "mio", "postcard", diff --git a/client/Cargo.toml b/client/Cargo.toml @@ -8,6 +8,7 @@ default-run = "client" anyhow = "1.0.104" common = { path = "../common/" } confy = "2.0.0" +lazy_static = "1.5.0" macroquad = { version = "0.4.16", features = ["glam-serde"] } mio = { version = "1.2.2", features = ["net", "os-poll"] } postcard = { version = "1.1.3", default-features = false, features = ["use-std"] } diff --git a/client/src/game.rs b/client/src/game.rs @@ -1,32 +0,0 @@ -use std::time::Duration; - -use common::game::{ActionKind, GameState, Player}; -use macroquad::prelude::*; - -pub(crate) fn handle_movement_input( - local_player: &mut Player, - dt: &Duration, -) -> Option<ActionKind> { - let velocity_x = if is_key_down(KeyCode::A) || is_key_down(KeyCode::Left) { - -GameState::MAX_PLAYER_SPEED_PX_S - } else if is_key_down(KeyCode::D) || is_key_down(KeyCode::Right) { - GameState::MAX_PLAYER_SPEED_PX_S - } else { - 0. - }; - let velocity_y = if is_key_down(KeyCode::W) || is_key_down(KeyCode::Up) { - -GameState::MAX_PLAYER_SPEED_PX_S - } else if is_key_down(KeyCode::S) || is_key_down(KeyCode::Down) { - GameState::MAX_PLAYER_SPEED_PX_S - } else { - 0. - }; - if velocity_x != 0. || velocity_y != 0. { - Some(ActionKind::Move { - new_position: local_player.position() - + Vec2::new(velocity_x, velocity_y) * dt.as_secs_f32(), - }) - } else { - None - } -} diff --git a/client/src/game/mod.rs b/client/src/game/mod.rs @@ -0,0 +1,103 @@ +use std::{ + collections::VecDeque, + sync::mpsc::{Receiver, Sender}, + time::{Duration, Instant, SystemTime}, +}; + +use crate::render::Camera; +use common::game::{Action, ActionKind, Player, Response, state::GameState}; +use macroquad::prelude::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +mod run; +mod state; + +const CONFY_APP_NAME: &str = "library-game"; +const CONFY_CONFIG_NAME: &str = "config"; + +#[derive(Debug)] +pub(crate) struct ClientState { + mode: ViewMode, + camera: Camera, + game: GameState, + old_game: GameState, + local_player_id: Option<Uuid>, + sequence_number: u32, + unresponded_actions: VecDeque<Action>, + interpolation_basis: SystemTime, + debug_view: bool, + t0: Instant, + t1: Instant, + dt: Duration, + tx: Option<Sender<Action>>, + rx: Option<Receiver<Response>>, + config: Config, + menu_error: String, + first_frame: bool, +} + +impl Default for ClientState { + fn default() -> Self { + Self { + mode: ViewMode::default(), + camera: Camera::default(), + game: GameState::default(), + old_game: GameState::default(), + local_player_id: None, + sequence_number: 0, + unresponded_actions: VecDeque::new(), + interpolation_basis: SystemTime::now(), + debug_view: cfg!(debug_assertions), + t0: Instant::now(), + t1: Instant::now(), + dt: Duration::ZERO, + tx: None, + rx: None, + config: confy::load(CONFY_APP_NAME, Some(CONFY_CONFIG_NAME)).unwrap_or_default(), + menu_error: String::new(), + first_frame: true, + } + } +} + +pub(crate) fn handle_movement_input( + local_player: &mut Player, + dt: &Duration, +) -> Option<ActionKind> { + let velocity_x = if is_key_down(KeyCode::A) || is_key_down(KeyCode::Left) { + -GameState::MAX_PLAYER_SPEED_PX_S + } else if is_key_down(KeyCode::D) || is_key_down(KeyCode::Right) { + GameState::MAX_PLAYER_SPEED_PX_S + } else { + 0. + }; + let velocity_y = if is_key_down(KeyCode::W) || is_key_down(KeyCode::Up) { + -GameState::MAX_PLAYER_SPEED_PX_S + } else if is_key_down(KeyCode::S) || is_key_down(KeyCode::Down) { + GameState::MAX_PLAYER_SPEED_PX_S + } else { + 0. + }; + if velocity_x != 0. || velocity_y != 0. { + Some(ActionKind::Move { + new_position: local_player.position() + + Vec2::new(velocity_x, velocity_y) * dt.as_secs_f32(), + }) + } else { + None + } +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +struct Config { + player_name: String, + server: String, +} + +#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub(crate) enum ViewMode { + #[default] + Menu, + Game, +} diff --git a/client/src/game/run.rs b/client/src/game/run.rs @@ -0,0 +1,142 @@ +use std::{net::SocketAddr, sync::mpsc::channel, thread}; + +use crate::{ + game::{CONFY_APP_NAME, CONFY_CONFIG_NAME, ClientState, ViewMode, handle_movement_input}, + net::handle_connection, + render::{follow_local_player, render_game}, +}; + +use common::game::{Action, Response}; +use macroquad::{ + prelude::*, + ui::{ + hash, root_ui, + widgets::{Button, InputText, Label}, + }, +}; + +impl ClientState { + pub(crate) fn run_menu_frame(&mut self) { + clear_background(MAGENTA); + const ELEMENT_SIZE: Vec2 = Vec2::new(420., 24.); + const NUM_ELEMENTS: usize = 4; + const MENU_SIZE: Vec2 = Vec2::new(ELEMENT_SIZE.x, ELEMENT_SIZE.y * NUM_ELEMENTS as f32); + const ELEMENT_PITCH: Vec2 = Vec2::new(0., ELEMENT_SIZE.y); + let screen_size = vec2(screen_width(), screen_height()); + let menu_top_left = screen_size / 2. - MENU_SIZE / 2.; + let mut ui = root_ui(); + let name_input_token = hash!(); + InputText::new(name_input_token) + .label("name") + .position(menu_top_left) + .size(ELEMENT_SIZE) + .margin(Vec2::ZERO) + .ui(&mut ui, &mut self.config.player_name); + InputText::new(hash!()) + .label("server") + .position(menu_top_left) + .size(ELEMENT_SIZE) + .margin(Vec2::ZERO) + .ui(&mut ui, &mut self.config.server); + let connect_button_clicked = Button::new("connect") + .position(menu_top_left + ELEMENT_PITCH * 2.) + .size(ELEMENT_SIZE) + .ui(&mut ui); + let addr = if connect_button_clicked || is_key_pressed(KeyCode::Enter) { + if let Ok(addr) = self.config.server.parse::<SocketAddr>() { + Some(addr) + } else if self.config.player_name.is_empty() { + self.menu_error = "enter your name pls".to_string(); + None + } else { + self.menu_error = "invalid server :-\\".to_string(); + None + } + } else { + None + }; + Label::new(self.menu_error.as_str()) + .position(menu_top_left + ELEMENT_PITCH * 3.) + .size(ELEMENT_SIZE) + .ui(&mut ui); + if self.first_frame { + ui.set_input_focus(name_input_token); + self.first_frame = false; + } + if let Some(addr) = addr { + let _ = confy::store(CONFY_APP_NAME, Some(CONFY_CONFIG_NAME), self.config.clone()); + let (client_to_server_tx, client_to_server_rx) = channel(); + let (server_to_client_tx, server_to_client_rx) = channel(); + thread::spawn(move || { + handle_connection(addr, server_to_client_tx, client_to_server_rx) + }); + self.tx = Some(client_to_server_tx); + self.rx = Some(server_to_client_rx); + self.mode = ViewMode::Game; + } + } + pub(crate) fn run_game_frame(&mut self) { + // Update frame timer so we move the player at the right speed + self.tick(); + + // Process any new responses from the server + let mut response_count: u8 = 0; + let mut latest_sequence_number = 0; + if let Some(ref rx) = self.rx { + for response in rx.try_iter().collect::<Vec<Response>>() { + response_count = response_count.saturating_add(1); + // If this is the first response that we're processing during this tick, + // then we need to copy the current game state so we can interpolate against + // it below + if response_count == 1 { + self.advance_game_state(); + } + self.handle_response(&response); + latest_sequence_number = latest_sequence_number.max(response.sequence_number()); + } + } + + // If there were any server responses, then do server reconciliation and client + // prediction + if response_count > 0 { + self.reconcile(latest_sequence_number); + self.predict(); + } + + if let Some(id) = self.local_player_id + && let Some(local_player) = self.game.get_player_mut(&id) + { + // Send movement actions if requested + if let Some(action_kind) = handle_movement_input(local_player, &self.dt) + && let Some(ref mut tx) = self.tx + { + let action = Action::new(self.sequence_number, id, action_kind); + self.game.apply(&action); + if tx.send(action.clone()).is_err() { + return; + } + self.unresponded_actions.push_back(action); + self.sequence_number = self.sequence_number.wrapping_add(1); + } + + // Center local player on the screen + follow_local_player(&mut self.camera, &self.game, &id); + } + + // Entity interpolation + let interpolated_game = self.interpolate(); + + // Toggle debug view if requested + if is_key_pressed(KeyCode::F3) { + self.debug_view = !self.debug_view; + } + + // Draw the frame + render_game( + &self.camera, + &interpolated_game, + self.debug_view, + self.local_player_id, + ); + } +} diff --git a/client/src/game/state.rs b/client/src/game/state.rs @@ -0,0 +1,75 @@ +use std::time::{Instant, SystemTime}; + +use common::game::{Action, ActionKind, Response, ResponseKind, state::GameState}; + +use crate::game::{ClientState, ViewMode}; + +impl ClientState { + pub(crate) fn view_mode(&self) -> ViewMode { + self.mode + } + pub(crate) fn tick(&mut self) { + self.t0 = self.t1; + self.t1 = Instant::now(); + self.dt = self.t1.duration_since(self.t0); + } + pub(crate) fn advance_game_state(&mut self) { + self.old_game = self.game.clone(); + } + pub(crate) fn handle_response(&mut self, response: &Response) { + match response.kind() { + ResponseKind::SetId { id } => { + let action = Action::new( + self.sequence_number, + id, + ActionKind::Join { + name: self.config.player_name.clone(), + }, + ); + if let Some(ref mut tx) = self.tx + && tx.send(action.clone()).is_err() + { + return; + } + self.unresponded_actions.push_back(action); + self.sequence_number = self.sequence_number.wrapping_add(1); + self.local_player_id = Some(id); + } + ResponseKind::UpdateGameState { updates } => { + for update in updates { + self.game.update(&update); + } + self.game.set_timestamp(response.timestamp()); + } + ResponseKind::ReplaceGameState { new_game_state } => self.game = new_game_state, + }; + } + pub(crate) fn reconcile(&mut self, latest_sequence_number: u32) { + // Server reconciliation: drop any unresponded actions whose sequence numbers are less than + // the number provided + if let Some(new_front_index) = self + .unresponded_actions + .iter() + .position(|ua: &Action| ua.sequence_number() >= latest_sequence_number) + { + for _ in 0..=new_front_index { + self.unresponded_actions.pop_front(); + } + } + } + pub(crate) fn predict(&mut self) { + // Client prediction: apply all unresponded actions to the game state + self.interpolation_basis = SystemTime::now(); + for unresponded_action in self.unresponded_actions.iter() { + self.game.apply(unresponded_action); + } + } + pub(crate) fn interpolate(&self) -> GameState { + GameState::interpolate( + &self.old_game, + &self.game, + self.interpolation_basis, + self.local_player_id, + ) + } +} diff --git a/client/src/main.rs b/client/src/main.rs @@ -1,302 +1,25 @@ -use std::{ - collections::VecDeque, - net::SocketAddr, - sync::mpsc::{Receiver, Sender, channel}, - thread, - time::{Duration, Instant, SystemTime}, -}; - -use common::game::{Action, ActionKind, GameState, Response, ResponseKind}; -use macroquad::{ - prelude::*, - ui::{ - hash, root_ui, - widgets::{Button, InputText, Label}, - }, -}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; +use macroquad::prelude::*; use crate::{ - game::handle_movement_input, - net::handle_connection, - render::{ - Camera, SCREEN_HEIGHT_PX, SCREEN_WIDTH_PX, Spritesheet, follow_local_player, render_game, - }, + game::{ClientState, ViewMode}, + render::{SCREEN_HEIGHT_PX, SCREEN_WIDTH_PX}, }; -pub mod game; -pub mod net; -pub mod render; - -const CONFY_APP_NAME: &str = "library-game"; -const CONFY_CONFIG_NAME: &str = "config"; - -#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] -struct Config { - player_name: String, - server: String, -} - -#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] -enum ViewMode { - #[default] - Menu, - Game, -} - -#[derive(Debug)] -struct ClientState { - mode: ViewMode, - camera: Camera, - game: GameState, - old_game: GameState, - local_player_id: Option<Uuid>, - sequence_number: u32, - unresponded_actions: VecDeque<Action>, - interpolation_basis: SystemTime, - debug_view: bool, - t0: Instant, - t1: Instant, - dt: Duration, - tx: Option<Sender<Action>>, - rx: Option<Receiver<Response>>, - config: Config, - menu_error: String, -} - -impl Default for ClientState { - fn default() -> Self { - Self { - mode: ViewMode::default(), - camera: Camera::default(), - game: GameState::default(), - old_game: GameState::default(), - local_player_id: None, - sequence_number: 0, - unresponded_actions: VecDeque::new(), - interpolation_basis: SystemTime::now(), - debug_view: cfg!(debug_assertions), - t0: Instant::now(), - t1: Instant::now(), - dt: Duration::ZERO, - tx: None, - rx: None, - config: confy::load(CONFY_APP_NAME, Some(CONFY_CONFIG_NAME)).unwrap_or_default(), - menu_error: String::new(), - } - } -} - -impl ClientState { - fn tick(&mut self) { - self.t0 = self.t1; - self.t1 = Instant::now(); - self.dt = self.t1.duration_since(self.t0); - } - fn advance_game_state(&mut self) { - self.old_game = self.game.clone(); - } - fn handle_response(&mut self, response: &Response) { - match response.kind() { - ResponseKind::SetId { id } => { - let action = Action::new( - self.sequence_number, - id, - ActionKind::Join { - name: self.config.player_name.clone(), - }, - ); - if let Some(ref mut tx) = self.tx - && tx.send(action.clone()).is_err() - { - return; - } - self.unresponded_actions.push_back(action); - self.sequence_number = self.sequence_number.wrapping_add(1); - self.local_player_id = Some(id); - } - ResponseKind::UpdateGameState { updates } => { - for update in updates { - self.game.update(&update); - } - self.game.set_timestamp(response.timestamp()); - } - ResponseKind::ReplaceGameState { new_game_state } => self.game = new_game_state, - }; - } - fn reconcile(&mut self, latest_sequence_number: u32) { - // Server reconciliation: drop any unresponded actions whose sequence numbers are less than - // the number provided - if let Some(new_front_index) = self - .unresponded_actions - .iter() - .position(|ua: &Action| ua.sequence_number() >= latest_sequence_number) - { - for _ in 0..=new_front_index { - self.unresponded_actions.pop_front(); - } - } - } - fn predict(&mut self) { - // Client prediction: apply all unresponded actions to the game state - self.interpolation_basis = SystemTime::now(); - for unresponded_action in self.unresponded_actions.iter() { - self.game.apply(unresponded_action); - } - } - fn interpolate(&self) -> GameState { - GameState::interpolate( - &self.old_game, - &self.game, - self.interpolation_basis, - self.local_player_id, - ) - } -} +mod game; +mod net; +mod render; #[macroquad::main("Library")] async fn main() { - // One-time startup actions request_new_screen_size(SCREEN_WIDTH_PX, SCREEN_HEIGHT_PX); - let spritesheet = Spritesheet::load(); let mut client = ClientState::default(); - let mut first_frame = true; loop { - match client.mode { - ViewMode::Menu => { - clear_background(MAGENTA); - const ELEMENT_SIZE: Vec2 = Vec2::new(420., 24.); - const NUM_ELEMENTS: usize = 4; - const MENU_SIZE: Vec2 = - Vec2::new(ELEMENT_SIZE.x, ELEMENT_SIZE.y * NUM_ELEMENTS as f32); - const ELEMENT_PITCH: Vec2 = Vec2::new(0., ELEMENT_SIZE.y); - let screen_size = vec2(screen_width(), screen_height()); - let menu_top_left = screen_size / 2. - MENU_SIZE / 2.; - let mut ui = root_ui(); - let name_input_token = hash!(); - InputText::new(name_input_token) - .label("name") - .position(menu_top_left) - .size(ELEMENT_SIZE) - .margin(Vec2::ZERO) - .ui(&mut ui, &mut client.config.player_name); - InputText::new(hash!()) - .label("server") - .position(menu_top_left) - .size(ELEMENT_SIZE) - .margin(Vec2::ZERO) - .ui(&mut ui, &mut client.config.server); - let connect_button_clicked = Button::new("connect") - .position(menu_top_left + ELEMENT_PITCH * 2.) - .size(ELEMENT_SIZE) - .ui(&mut ui); - let addr = if connect_button_clicked || is_key_pressed(KeyCode::Enter) { - if let Ok(addr) = client.config.server.parse::<SocketAddr>() { - Some(addr) - } else if client.config.player_name.is_empty() { - client.menu_error = "enter your name pls".to_string(); - None - } else { - client.menu_error = "invalid server :-\\".to_string(); - None - } - } else { - None - }; - Label::new(client.menu_error.as_str()) - .position(menu_top_left + ELEMENT_PITCH * 3.) - .size(ELEMENT_SIZE) - .ui(&mut ui); - if first_frame { - ui.set_input_focus(name_input_token); - } - if let Some(addr) = addr { - let _ = confy::store( - CONFY_APP_NAME, - Some(CONFY_CONFIG_NAME), - client.config.clone(), - ); - let (client_to_server_tx, client_to_server_rx) = channel(); - let (server_to_client_tx, server_to_client_rx) = channel(); - thread::spawn(move || { - handle_connection(addr, server_to_client_tx, client_to_server_rx) - }); - client.tx = Some(client_to_server_tx); - client.rx = Some(server_to_client_rx); - client.mode = ViewMode::Game; - } - } - ViewMode::Game => { - // Update frame timer so we move the player at the right speed - client.tick(); - - // Process any new responses from the server - let mut response_count: u8 = 0; - let mut latest_sequence_number = 0; - if let Some(ref rx) = client.rx { - for response in rx.try_iter().collect::<Vec<Response>>() { - response_count = response_count.saturating_add(1); - // If this is the first response that we're processing during this tick, - // then we need to copy the current game state so we can interpolate against - // it below - if response_count == 1 { - client.advance_game_state(); - } - client.handle_response(&response); - latest_sequence_number = - latest_sequence_number.max(response.sequence_number()); - } - } - - // If there were any server responses, then do server reconciliation and client - // prediction - if response_count > 0 { - client.reconcile(latest_sequence_number); - client.predict(); - } - - if let Some(id) = client.local_player_id - && let Some(local_player) = client.game.get_player_mut(&id) - { - // Send movement actions if requested - if let Some(action_kind) = handle_movement_input(local_player, &client.dt) - && let Some(ref mut tx) = client.tx - { - let action = Action::new(client.sequence_number, id, action_kind); - client.game.apply(&action); - if tx.send(action.clone()).is_err() { - return; - } - client.unresponded_actions.push_back(action); - client.sequence_number = client.sequence_number.wrapping_add(1); - } - - // Center local player on the screen - follow_local_player(&mut client.camera, &client.game, &spritesheet, &id); - } - - // Entity interpolation - let interpolated_game = client.interpolate(); - - // Toggle debug view if requested - if is_key_pressed(KeyCode::F3) { - client.debug_view = !client.debug_view; - } - - // Draw the frame - render_game( - &client.camera, - &interpolated_game, - &spritesheet, - client.debug_view, - client.local_player_id, - ); - } + match client.view_mode() { + ViewMode::Menu => client.run_menu_frame(), + ViewMode::Game => client.run_game_frame(), } - first_frame = false; next_frame().await; } } diff --git a/client/src/render.rs b/client/src/render.rs @@ -1,7 +1,12 @@ -use common::game::{BOOKS, GameState}; +use common::game::{book::BOOKS, state::GameState}; +use lazy_static::lazy_static; use macroquad::prelude::*; use uuid::Uuid; +lazy_static! { + pub(crate) static ref SPRITESHEET: Spritesheet = Spritesheet::load(); +} + pub const SCREEN_WIDTH_PX: f32 = 1280.; pub const SCREEN_HEIGHT_PX: f32 = 720.; @@ -71,7 +76,6 @@ impl Spritesheet { pub(crate) fn render_game( camera: &Camera, game: &GameState, - spritesheet: &Spritesheet, debug_view: bool, local_player_id: Option<Uuid>, ) { @@ -84,14 +88,14 @@ pub(crate) fn render_game( } // Render books on the floor - let floor_book_sprite = spritesheet.book.clone(); + let floor_book_sprite = SPRITESHEET.book.clone(); for floor_book in game.floor_books().iter() { let position_camera = floor_book.position() - camera.position; floor_book_sprite.render(position_camera); } // Render players - let player_sprite = spritesheet.meeple.clone(); + let player_sprite = SPRITESHEET.meeple.clone(); for player in game.players().values() { let position_camera = player.position() - camera.position; player_sprite.render(position_camera); @@ -173,15 +177,10 @@ pub(crate) fn render_game( } } -pub(crate) fn follow_local_player( - camera: &mut Camera, - game: &GameState, - spritesheet: &Spritesheet, - local_player_id: &Uuid, -) { +pub(crate) fn follow_local_player(camera: &mut Camera, game: &GameState, local_player_id: &Uuid) { let screen_center = Vec2::new(screen_width() / 2., screen_height() / 2.); if let Some((_, player)) = game.players().iter().find(|(id, _)| *id == local_player_id) { - let player_sprite = &spritesheet.meeple; + let player_sprite = &SPRITESHEET.meeple; camera.position = player.position() - screen_center + player_sprite.size / 2.; } } diff --git a/common/src/game.rs b/common/src/game.rs @@ -1,512 +0,0 @@ -use std::{ - collections::HashMap, - fmt::Display, - io::Write, - time::{Duration, SystemTime}, -}; - -use anyhow::{Result, anyhow}; -use lazy_static::lazy_static; -use macroquad::{ - math::{Rect, Vec2}, - rand::{rand, srand}, -}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -lazy_static! { - pub static ref BOOKS: BookDatabase = - BookDatabase::from_csv(include_str!("../assets/books.csv")) - .expect("Failed to parse book database"); -} - -pub const TICK_DURATION: Duration = Duration::from_micros(16_667); - -pub type BookId = Uuid; -pub type Players = HashMap<Uuid, Player>; - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct GameState { - timestamp: SystemTime, - players: Players, - bookshelves: Vec<Bookshelf>, - floor_books: Vec<FloorBook>, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub enum Update { - InsertPlayer { id: Uuid, player: Player }, - RemovePlayer { player_id: Uuid }, -} - -/// Equivalent to [macroquad::math::Rect::overlaps], except using strict comparison (i.e., `<` and -/// `>` instead of `<=` and `>=`) -fn rects_strictly_overlap(a: &Rect, b: &Rect) -> bool { - a.left() < b.right() && a.right() > b.left() && a.top() < b.bottom() && a.bottom() > b.top() -} - -impl GameState { - pub const MAX_PLAYER_SPEED_PX_S: f32 = 75.; - - pub fn update(&mut self, update: &Update) { - match update { - Update::InsertPlayer { id, player } => self.players.insert(*id, player.clone()), - Update::RemovePlayer { player_id } => self.players.remove(player_id), - }; - } - pub fn apply(&mut self, action: &Action) { - match action.kind() { - ActionKind::Join { name } => { - self.players.insert(action.player_id, Player::new(name)); - } - ActionKind::Move { new_position } => { - if let Some(player) = self.players.get_mut(&action.player_id) - && let Ok(dt) = action.timestamp.duration_since(player.timestamp) - { - let velocity = (new_position - player.position) / dt.as_secs_f32(); - let limited_velocity = if velocity.length() > GameState::MAX_PLAYER_SPEED_PX_S { - velocity.normalize() * GameState::MAX_PLAYER_SPEED_PX_S - } else { - velocity - }; - player.position += limited_velocity * dt.as_secs_f32(); - - // Handle collisions with bookshelves - let player_collider = player.collider.rect().offset(player.position); - for bookshelf in self.bookshelves.iter() { - // If the player's movement would cause their collider to intersect with a - // bookshelf's collider, then compute the minimum axis-aligned motion - // required to make them touch but not intersect - let bookshelf_collider: Rect = bookshelf.bounds.into(); - let (a, b) = (bookshelf_collider, player_collider); - if rects_strictly_overlap(&a, &b) { - // Compute the required motion in each possible direction and select the - // smallest among them - let up = Vec2::new(0., a.top() - b.bottom()); - let down = Vec2::new(0., a.bottom() - b.top()); - let left = Vec2::new(a.left() - b.right(), 0.); - let right = Vec2::new(a.right() - b.left(), 0.); - let min_y = if up.y.abs() < down.y.abs() { up } else { down }; - let min_x = if right.x.abs() < left.x.abs() { - right - } else { - left - }; - let min = if min_x.x.abs() < min_y.y.abs() { - min_x - } else { - min_y - }; - player.position += min; - } - } - } - } - } - } - pub fn interpolate( - old: &Self, - new: &Self, - basis: SystemTime, - local_player_id: Option<Uuid>, - ) -> GameState { - let mut output = GameState::default(); - for (new_id, new_player) in new.players.iter() { - if let Some(old_player) = old.players.get(new_id) { - if let Some(ref lpid) = local_player_id - && new_id == lpid - { - output.players.insert(*new_id, new_player.clone()); - } else { - let span = new.timestamp.duration_since(old.timestamp); - let fraction = SystemTime::now().duration_since(basis); - let t = match (fraction, span) { - (Ok(f), Ok(s)) => (f.as_secs_f32() / s.as_secs_f32()).clamp(0., 1.), - _ => return new.clone(), - }; - let interpolated_player = Player { - position: (new_player.position() - old_player.position()) * t - + old_player.position(), - ..new_player.clone() - }; - output.players.insert(*new_id, interpolated_player); - } - } else { - output.players.insert(*new_id, new_player.clone()); - } - } - output - } - pub fn get_player(&self, id: &Uuid) -> Option<&Player> { - self.players.get(id) - } - pub fn get_player_mut(&mut self, id: &Uuid) -> Option<&mut Player> { - self.players.get_mut(id) - } - pub fn players(&self) -> &HashMap<Uuid, Player> { - &self.players - } - pub fn insert_player(&mut self, id: Uuid, player: Player) -> Option<Player> { - self.players.insert(id, player) - } - pub fn remove_player(&mut self, id: &Uuid) -> Option<Player> { - self.players.remove(id) - } - pub fn set_timestamp(&mut self, timestamp: SystemTime) { - self.timestamp = timestamp; - } - pub fn get_timestamp(&self) -> SystemTime { - self.timestamp - } - pub fn update_timestamp(&mut self) { - self.set_timestamp(SystemTime::now()); - } - pub fn bookshelves(&self) -> &Vec<Bookshelf> { - self.bookshelves.as_ref() - } - pub fn floor_books(&self) -> &Vec<FloorBook> { - self.floor_books.as_ref() - } -} - -impl Default for GameState { - fn default() -> Self { - Self { - timestamp: SystemTime::now(), - players: HashMap::with_capacity(32), - bookshelves: vec![ - Bookshelf { - bounds: RectSerde { - x: -200., - y: -100., - w: 100., - h: 500., - }, - }, - Bookshelf { - bounds: RectSerde { - x: -400., - y: -100., - w: 100., - h: 500., - }, - }, - ], - floor_books: { - srand(0); - BOOKS - .books - .keys() - .map(|&id| FloorBook { - id, - position: Vec2::new((rand() % 750) as f32, (rand() % 750) as f32), - collider: Rect::new(0., 0., 30., 23.6).into(), - }) - .collect() - }, - } - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct Player { - name: String, - position: Vec2, - collider: RectSerde, - timestamp: SystemTime, -} - -impl Player { - fn new(name: String) -> Self { - Player { - name, - position: Vec2::ZERO, - collider: RectSerde::new(0., 0., 43.4, 40.2), - timestamp: SystemTime::now(), - } - } - pub fn name(&self) -> String { - self.name.clone() - } - pub fn position(&self) -> Vec2 { - self.position - } - pub fn collider(&self) -> RectSerde { - self.collider - } -} - -#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)] -pub struct RectSerde { - x: f32, - y: f32, - w: f32, - h: f32, -} - -impl RectSerde { - pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self { - RectSerde { x, y, w, h } - } - pub fn rect(&self) -> Rect { - (*self).into() - } -} - -impl From<Rect> for RectSerde { - fn from(value: Rect) -> Self { - RectSerde { - x: value.x, - y: value.y, - w: value.w, - h: value.h, - } - } -} - -impl From<RectSerde> for Rect { - fn from(value: RectSerde) -> Self { - Rect::new(value.x, value.y, value.w, value.h) - } -} - -#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)] -pub struct Bookshelf { - bounds: RectSerde, -} - -impl Bookshelf { - pub fn rect(&self) -> Rect { - self.bounds.into() - } -} - -#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct FloorBook { - id: BookId, - position: Vec2, - collider: RectSerde, -} - -impl FloorBook { - pub fn id(&self) -> BookId { - self.id - } - pub fn position(&self) -> Vec2 { - self.position - } - pub fn collider(&self) -> RectSerde { - self.collider - } -} - -pub struct BookDatabase { - books: HashMap<BookId, BookMetadata>, -} - -fn _csv_escape(input: &String) -> String { - if input.contains(',') { - format!("\"{input}\"") - } else { - input.clone() - } -} - -impl BookDatabase { - const CSV_HEADER: &str = "id,title,author,publish_year"; - - fn new() -> Self { - BookDatabase { - books: HashMap::new(), - } - } - fn _to_csv(&self, output: &mut impl Write) -> Result<()> { - writeln!(output, "{}", BookDatabase::CSV_HEADER)?; - for (id, metadata) in self.books.iter() { - let BookMetadata { - title, - author, - publish_year, - } = metadata; - writeln!( - output, - "{},{},{},{}", - id, - _csv_escape(title), - _csv_escape(author), - _csv_escape(publish_year), - )?; - } - Ok(()) - } - fn from_csv(input: &str) -> Result<Self> { - let mut output = BookDatabase::new(); - for (line_index, line) in input.lines().enumerate() { - if line_index == 0 { - if line != BookDatabase::CSV_HEADER { - return Err(anyhow!( - "Invalid CSV header: expected '{}' but got '{}'", - BookDatabase::CSV_HEADER, - line - )); - } - continue; - } - let mut fields = vec![]; - #[derive(Clone, Copy, Debug)] - enum State { - Start, - FieldData, - Quote, - FieldEnd, - } - let mut state = State::Start; - let mut new_field = String::new(); - let mut quoted_field = false; - for c in line.chars() { - match (c, state) { - ('"', State::Start | State::FieldEnd) => { - quoted_field = true; - state = State::FieldData; - } - ('"', State::FieldData) => state = State::Quote, - ('"', State::Quote) => { - if quoted_field { - new_field.push('"'); - } - } - (',', State::Start | State::FieldEnd) => state = State::FieldEnd, - (',', State::FieldData) => { - if quoted_field { - new_field.push(','); - } else { - state = State::FieldEnd; - } - } - (',', State::Quote) => { - if quoted_field { - state = State::FieldEnd; - } - } - (d, _) => { - new_field.push(d); - state = State::FieldData; - } - } - if let State::FieldEnd = state { - fields.push(new_field.clone()); - new_field.clear(); - quoted_field = false; - } - } - fields.push(new_field); - if fields.len() != 4 { - return Err(anyhow!( - "Error on line {}: expected 4 fields but got {}", - line_index + 1, - fields.len() - )); - } - let id = fields[0].parse()?; - let title = fields[1].clone(); - let author = fields[2].clone(); - let publish_year = fields[3].clone(); - output.books.insert( - id, - BookMetadata { - title, - author, - publish_year, - }, - ); - } - Ok(output) - } - pub fn get(&self, k: &BookId) -> Option<&BookMetadata> { - self.books.get(k) - } -} - -#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct BookMetadata { - title: String, - author: String, - publish_year: String, -} - -impl Display for BookMetadata { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} by {} ({})", - self.title, self.author, self.publish_year - ) - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct Action { - sequence_number: u32, - timestamp: SystemTime, - player_id: Uuid, - kind: ActionKind, -} - -impl Action { - pub fn new(sequence_number: u32, player_id: Uuid, kind: ActionKind) -> Self { - Action { - sequence_number, - timestamp: SystemTime::now(), - player_id, - kind, - } - } - pub fn sequence_number(&self) -> u32 { - self.sequence_number - } - pub fn player_id(&self) -> Uuid { - self.player_id - } - pub fn kind(&self) -> ActionKind { - self.kind.clone() - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub enum ActionKind { - Join { name: String }, - Move { new_position: Vec2 }, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct Response { - timestamp: SystemTime, - sequence_number: u32, - kind: ResponseKind, -} - -impl Response { - pub fn new(timestamp: SystemTime, sequence_number: u32, kind: ResponseKind) -> Self { - Response { - timestamp, - sequence_number, - kind, - } - } - pub fn timestamp(&self) -> SystemTime { - self.timestamp - } - pub fn sequence_number(&self) -> u32 { - self.sequence_number - } - pub fn kind(&self) -> ResponseKind { - self.kind.clone() - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub enum ResponseKind { - SetId { id: Uuid }, - UpdateGameState { updates: Vec<Update> }, - ReplaceGameState { new_game_state: GameState }, -} diff --git a/common/src/game/book.rs b/common/src/game/book.rs @@ -0,0 +1,212 @@ +use std::{ + collections::{HashMap, hash_map::Keys}, + fmt::{Display, Write}, +}; + +use anyhow::{Result, anyhow}; +use lazy_static::lazy_static; +use macroquad::math::{Rect, Vec2}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::game::RectSerde; + +lazy_static! { + pub static ref BOOKS: BookDatabase = + BookDatabase::from_csv(include_str!("../../assets/books.csv")) + .expect("Failed to parse book database"); +} + +pub type BookId = Uuid; + +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)] +pub struct Bookshelf { + bounds: RectSerde, +} + +impl Bookshelf { + pub(crate) fn new(bounds: Rect) -> Self { + Self { + bounds: bounds.into(), + } + } + pub fn rect(&self) -> Rect { + self.bounds.into() + } + pub(crate) fn bounds(&self) -> RectSerde { + self.bounds + } +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct FloorBook { + id: BookId, + position: Vec2, + collider: RectSerde, +} + +impl FloorBook { + pub(crate) fn new(id: BookId, position: Vec2, collider: Rect) -> Self { + Self { + id, + position, + collider: collider.into(), + } + } + pub fn id(&self) -> BookId { + self.id + } + pub fn position(&self) -> Vec2 { + self.position + } + pub fn collider(&self) -> RectSerde { + self.collider + } +} + +pub struct BookDatabase { + books: HashMap<BookId, BookMetadata>, +} + +fn _csv_escape(input: &String) -> String { + if input.contains(',') { + format!("\"{input}\"") + } else { + input.clone() + } +} + +impl BookDatabase { + const CSV_HEADER: &str = "id,title,author,publish_year"; + + fn new() -> Self { + BookDatabase { + books: HashMap::new(), + } + } + pub(crate) fn keys(&self) -> Keys<'_, BookId, BookMetadata> { + self.books.keys() + } + fn _to_csv(&self, output: &mut impl Write) -> Result<()> { + writeln!(output, "{}", BookDatabase::CSV_HEADER)?; + for (id, metadata) in self.books.iter() { + let BookMetadata { + title, + author, + publish_year, + } = metadata; + writeln!( + output, + "{},{},{},{}", + id, + _csv_escape(title), + _csv_escape(author), + _csv_escape(publish_year), + )?; + } + Ok(()) + } + fn from_csv(input: &str) -> Result<Self> { + let mut output = BookDatabase::new(); + for (line_index, line) in input.lines().enumerate() { + if line_index == 0 { + if line != BookDatabase::CSV_HEADER { + return Err(anyhow!( + "Invalid CSV header: expected '{}' but got '{}'", + BookDatabase::CSV_HEADER, + line + )); + } + continue; + } + let mut fields = vec![]; + #[derive(Clone, Copy, Debug)] + enum State { + Start, + FieldData, + Quote, + FieldEnd, + } + let mut state = State::Start; + let mut new_field = String::new(); + let mut quoted_field = false; + for c in line.chars() { + match (c, state) { + ('"', State::Start | State::FieldEnd) => { + quoted_field = true; + state = State::FieldData; + } + ('"', State::FieldData) => state = State::Quote, + ('"', State::Quote) => { + if quoted_field { + new_field.push('"'); + } + } + (',', State::Start | State::FieldEnd) => state = State::FieldEnd, + (',', State::FieldData) => { + if quoted_field { + new_field.push(','); + } else { + state = State::FieldEnd; + } + } + (',', State::Quote) => { + if quoted_field { + state = State::FieldEnd; + } + } + (d, _) => { + new_field.push(d); + state = State::FieldData; + } + } + if let State::FieldEnd = state { + fields.push(new_field.clone()); + new_field.clear(); + quoted_field = false; + } + } + fields.push(new_field); + if fields.len() != 4 { + return Err(anyhow!( + "Error on line {}: expected 4 fields but got {}", + line_index + 1, + fields.len() + )); + } + let id = fields[0].parse()?; + let title = fields[1].clone(); + let author = fields[2].clone(); + let publish_year = fields[3].clone(); + output.books.insert( + id, + BookMetadata { + title, + author, + publish_year, + }, + ); + } + Ok(output) + } + pub fn get(&self, k: &BookId) -> Option<&BookMetadata> { + self.books.get(k) + } +} + +#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct BookMetadata { + title: String, + author: String, + publish_year: String, +} + +impl Display for BookMetadata { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} by {} ({})", + self.title, self.author, self.publish_year + ) + } +} diff --git a/common/src/game/mod.rs b/common/src/game/mod.rs @@ -0,0 +1,151 @@ +use std::{ + collections::HashMap, + time::{Duration, SystemTime}, +}; + +use macroquad::math::{Rect, Vec2}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::game::state::{GameState, Update}; + +pub mod book; +pub mod state; + +pub const TICK_DURATION: Duration = Duration::from_micros(16_667); + +pub type Players = HashMap<Uuid, Player>; + +/// Equivalent to [macroquad::math::Rect::overlaps], except using strict comparison (i.e., `<` and +/// `>` instead of `<=` and `>=`) +fn rects_strictly_overlap(a: &Rect, b: &Rect) -> bool { + a.left() < b.right() && a.right() > b.left() && a.top() < b.bottom() && a.bottom() > b.top() +} +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Player { + name: String, + position: Vec2, + collider: RectSerde, + timestamp: SystemTime, +} + +impl Player { + fn new(name: String) -> Self { + Player { + name, + position: Vec2::ZERO, + collider: RectSerde::new(0., 0., 43.4, 40.2), + timestamp: SystemTime::now(), + } + } + pub fn name(&self) -> String { + self.name.clone() + } + pub fn position(&self) -> Vec2 { + self.position + } + pub fn collider(&self) -> RectSerde { + self.collider + } +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)] +pub struct RectSerde { + x: f32, + y: f32, + w: f32, + h: f32, +} + +impl RectSerde { + pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self { + RectSerde { x, y, w, h } + } + pub fn rect(&self) -> Rect { + (*self).into() + } +} + +impl From<Rect> for RectSerde { + fn from(value: Rect) -> Self { + RectSerde { + x: value.x, + y: value.y, + w: value.w, + h: value.h, + } + } +} + +impl From<RectSerde> for Rect { + fn from(value: RectSerde) -> Self { + Rect::new(value.x, value.y, value.w, value.h) + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Action { + sequence_number: u32, + timestamp: SystemTime, + player_id: Uuid, + kind: ActionKind, +} + +impl Action { + pub fn new(sequence_number: u32, player_id: Uuid, kind: ActionKind) -> Self { + Action { + sequence_number, + timestamp: SystemTime::now(), + player_id, + kind, + } + } + pub fn sequence_number(&self) -> u32 { + self.sequence_number + } + pub fn player_id(&self) -> Uuid { + self.player_id + } + pub fn kind(&self) -> ActionKind { + self.kind.clone() + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum ActionKind { + Join { name: String }, + Move { new_position: Vec2 }, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Response { + timestamp: SystemTime, + sequence_number: u32, + kind: ResponseKind, +} + +impl Response { + pub fn new(timestamp: SystemTime, sequence_number: u32, kind: ResponseKind) -> Self { + Response { + timestamp, + sequence_number, + kind, + } + } + pub fn timestamp(&self) -> SystemTime { + self.timestamp + } + pub fn sequence_number(&self) -> u32 { + self.sequence_number + } + pub fn kind(&self) -> ResponseKind { + self.kind.clone() + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum ResponseKind { + SetId { id: Uuid }, + UpdateGameState { updates: Vec<Update> }, + ReplaceGameState { new_game_state: GameState }, +} diff --git a/common/src/game/state.rs b/common/src/game/state.rs @@ -0,0 +1,178 @@ +use std::{collections::HashMap, time::SystemTime}; + +use macroquad::{ + math::{Rect, Vec2}, + rand::{rand, srand}, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::game::{ + Action, ActionKind, Player, Players, + book::{BOOKS, Bookshelf, FloorBook}, + rects_strictly_overlap, +}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct GameState { + timestamp: SystemTime, + players: Players, + bookshelves: Vec<Bookshelf>, + floor_books: Vec<FloorBook>, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum Update { + InsertPlayer { id: Uuid, player: Player }, + RemovePlayer { player_id: Uuid }, +} + +impl GameState { + pub const MAX_PLAYER_SPEED_PX_S: f32 = 75.; + + pub fn update(&mut self, update: &Update) { + match update { + Update::InsertPlayer { id, player } => self.players.insert(*id, player.clone()), + Update::RemovePlayer { player_id } => self.players.remove(player_id), + }; + } + pub fn apply(&mut self, action: &Action) { + match action.kind() { + ActionKind::Join { name } => { + self.players.insert(action.player_id, Player::new(name)); + } + ActionKind::Move { new_position } => { + if let Some(player) = self.players.get_mut(&action.player_id) + && let Ok(dt) = action.timestamp.duration_since(player.timestamp) + { + let velocity = (new_position - player.position) / dt.as_secs_f32(); + let limited_velocity = if velocity.length() > GameState::MAX_PLAYER_SPEED_PX_S { + velocity.normalize() * GameState::MAX_PLAYER_SPEED_PX_S + } else { + velocity + }; + player.position += limited_velocity * dt.as_secs_f32(); + + // Handle collisions with bookshelves + let player_collider = player.collider.rect().offset(player.position); + for bookshelf in self.bookshelves.iter() { + // If the player's movement would cause their collider to intersect with a + // bookshelf's collider, then compute the minimum axis-aligned motion + // required to make them touch but not intersect + let bookshelf_collider: Rect = bookshelf.bounds().into(); + let (a, b) = (bookshelf_collider, player_collider); + if rects_strictly_overlap(&a, &b) { + // Compute the required motion in each possible direction and select the + // smallest among them + let up = Vec2::new(0., a.top() - b.bottom()); + let down = Vec2::new(0., a.bottom() - b.top()); + let left = Vec2::new(a.left() - b.right(), 0.); + let right = Vec2::new(a.right() - b.left(), 0.); + let min_y = if up.y.abs() < down.y.abs() { up } else { down }; + let min_x = if right.x.abs() < left.x.abs() { + right + } else { + left + }; + let min = if min_x.x.abs() < min_y.y.abs() { + min_x + } else { + min_y + }; + player.position += min; + } + } + } + } + } + } + pub fn interpolate( + old: &Self, + new: &Self, + basis: SystemTime, + local_player_id: Option<Uuid>, + ) -> GameState { + let mut output = GameState::default(); + for (new_id, new_player) in new.players.iter() { + if let Some(old_player) = old.players.get(new_id) { + if let Some(ref lpid) = local_player_id + && new_id == lpid + { + output.players.insert(*new_id, new_player.clone()); + } else { + let span = new.timestamp.duration_since(old.timestamp); + let fraction = SystemTime::now().duration_since(basis); + let t = match (fraction, span) { + (Ok(f), Ok(s)) => (f.as_secs_f32() / s.as_secs_f32()).clamp(0., 1.), + _ => return new.clone(), + }; + let interpolated_player = Player { + position: (new_player.position() - old_player.position()) * t + + old_player.position(), + ..new_player.clone() + }; + output.players.insert(*new_id, interpolated_player); + } + } else { + output.players.insert(*new_id, new_player.clone()); + } + } + output + } + pub fn get_player(&self, id: &Uuid) -> Option<&Player> { + self.players.get(id) + } + pub fn get_player_mut(&mut self, id: &Uuid) -> Option<&mut Player> { + self.players.get_mut(id) + } + pub fn players(&self) -> &HashMap<Uuid, Player> { + &self.players + } + pub fn insert_player(&mut self, id: Uuid, player: Player) -> Option<Player> { + self.players.insert(id, player) + } + pub fn remove_player(&mut self, id: &Uuid) -> Option<Player> { + self.players.remove(id) + } + pub fn set_timestamp(&mut self, timestamp: SystemTime) { + self.timestamp = timestamp; + } + pub fn get_timestamp(&self) -> SystemTime { + self.timestamp + } + pub fn update_timestamp(&mut self) { + self.set_timestamp(SystemTime::now()); + } + pub fn bookshelves(&self) -> &Vec<Bookshelf> { + self.bookshelves.as_ref() + } + pub fn floor_books(&self) -> &Vec<FloorBook> { + self.floor_books.as_ref() + } +} + +impl Default for GameState { + fn default() -> Self { + Self { + timestamp: SystemTime::now(), + players: HashMap::with_capacity(32), + bookshelves: vec![ + Bookshelf::new(Rect::new(-200., -100., 100., 500.)), + Bookshelf::new(Rect::new(-400., -100., 100., 500.)), + ], + floor_books: { + srand(0); + BOOKS + .keys() + .map(|&id| { + FloorBook::new( + id, + Vec2::new((rand() % 750) as f32, (rand() % 750) as f32), + Rect::new(0., 0., 30., 23.6), + ) + }) + .collect() + }, + } + } +} diff --git a/server/src/main.rs b/server/src/main.rs @@ -5,7 +5,10 @@ use std::{ time::Instant, }; -use common::game::{ActionKind, GameState, Response, ResponseKind, TICK_DURATION, Update}; +use common::game::{ + ActionKind, Response, ResponseKind, TICK_DURATION, + state::{GameState, Update}, +}; use uuid::Uuid; use crate::net::listen;