library

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

commit dee0edbf6f241b54110cfcf6136cdeff7678cb78
parent 28136067d780b562c22447c6c5aa7b067595ba96
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Mon, 24 Aug 2026 21:17:27 -0400

Scatter some books on the floor and make other improvements

Diffstat:
MCargo.lock | 7+++++++
Aclient/assets/book.png | 0
Dclient/assets/books.png | 0
Mclient/src/game.rs | 2+-
Mclient/src/main.rs | 6+++---
Mclient/src/render.rs | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mcommon/Cargo.toml | 1+
Acommon/assets/books.csv | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/src/game.rs | 267++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mcommon/src/net.rs | 78++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mserver/src/main.rs | 6+++---
11 files changed, 445 insertions(+), 77 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -89,6 +89,7 @@ name = "common" version = "0.1.0" dependencies = [ "anyhow", + "lazy_static", "macroquad", "mio", "postcard", @@ -247,6 +248,12 @@ dependencies = [ ] [[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] name = "libc" version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/client/assets/book.png b/client/assets/book.png Binary files differ. diff --git a/client/assets/books.png b/client/assets/books.png Binary files differ. diff --git a/client/src/game.rs b/client/src/game.rs @@ -23,7 +23,7 @@ pub(crate) fn handle_movement_input( }; if velocity_x != 0. || velocity_y != 0. { Some(ActionKind::Move { - new_position: local_player.position + new_position: local_player.position() + Vec2::new(velocity_x, velocity_y) * dt.as_secs_f32(), }) } else { diff --git a/client/src/main.rs b/client/src/main.rs @@ -11,7 +11,7 @@ use macroquad::prelude::*; use crate::{ game::handle_movement_input, net::handle_connection, - render::{Camera, Spritesheet, follow_local_player, render}, + render::{Camera, SCREEN_HEIGHT_PX, SCREEN_WIDTH_PX, Spritesheet, follow_local_player, render}, }; pub mod game; @@ -20,7 +20,7 @@ pub mod render; #[macroquad::main("Library")] async fn main() { - request_new_screen_size(1280., 720.); + request_new_screen_size(SCREEN_WIDTH_PX, SCREEN_HEIGHT_PX); let spritesheet = Spritesheet::load(); let (client_to_server_tx, client_to_server_rx) = channel(); @@ -93,7 +93,7 @@ async fn main() { }; } if let Some(id) = local_player_id - && let Some(local_player) = game_state.players.get_mut(&id) + && let Some(local_player) = game_state.get_player_mut(&id) { if let Some(action_kind) = handle_movement_input(local_player, &dt) { let action = Action::new(sequence_number, id, action_kind); diff --git a/client/src/render.rs b/client/src/render.rs @@ -1,9 +1,12 @@ use std::ops::Index; -use common::game::{GameState, SpriteIndex}; +use common::game::{BOOKS, GameState, SpriteIndex}; use macroquad::prelude::*; use uuid::Uuid; +pub const SCREEN_WIDTH_PX: f32 = 1280.; +pub const SCREEN_HEIGHT_PX: f32 = 720.; + #[derive(Debug, Default)] pub(crate) struct Camera { position: Vec2, @@ -12,6 +15,7 @@ pub(crate) struct Camera { #[derive(Debug)] pub(crate) struct Spritesheet { meeple: Sprite, + book: Sprite, } #[derive(Debug, Clone)] @@ -54,7 +58,13 @@ impl Spritesheet { include_bytes!("../assets/meeple.png"), Some(ImageFormat::Png), ) - .unwrap() + .expect("Failed to load meeple.png") + .into(), + book: Image::from_file_with_format( + include_bytes!("../assets/book.png"), + Some(ImageFormat::Png), + ) + .expect("Failed to load book.png") .into(), } } @@ -66,6 +76,7 @@ impl Index<SpriteIndex> for Spritesheet { fn index(&self, index: SpriteIndex) -> &Self::Output { match index { SpriteIndex::Meeple => &self.meeple, + SpriteIndex::FloorBook => &self.book, } } } @@ -78,34 +89,93 @@ pub(crate) fn render( local_player_id: Option<Uuid>, ) { clear_background(MAGENTA); - for bookshelf in game.bookshelves.iter() { + + // Render bookshelves + for bookshelf in game.bookshelves().iter() { let bounds: Rect = bookshelf.rect().offset(-camera.position); draw_rectangle(bounds.x, bounds.y, bounds.w, bounds.h, DARKBROWN); } - for player in game.players.values() { - let sprite = match player.sprite_index { - SpriteIndex::Meeple => spritesheet.meeple.clone(), - }; - let position_camera = player.position - camera.position; - sprite.render(position_camera); + + // Render books on the floor + let floor_book_sprite = spritesheet[SpriteIndex::FloorBook].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[SpriteIndex::Meeple].clone(); + for player in game.players().values() { + let position_camera = player.position() - camera.position; + player_sprite.render(position_camera); draw_text( - player.name.clone(), + player.name().clone(), position_camera.x + 12., - position_camera.y + sprite.size.y + 16., + position_camera.y + player_sprite.size.y + 16., 16., BLACK, ); } + + // Render book metadata popup if local player is near a floor book + if let Some(id) = local_player_id + && let Some(local_player) = game.get_player(&id) + && let Some(nearby_floor_book) = game.floor_books().iter().find(|floor_book| { + floor_book + .collider() + .rect() + .offset(floor_book.position()) + .overlaps( + &local_player + .collider() + .rect() + .offset(local_player.position()), + ) + }) + { + let text = if let Some(metadata) = BOOKS.get(&nearby_floor_book.id()) { + metadata.to_string() + } else { + "Unknown book".to_string() + }; + let font_size = 24.; + let text_dimensions = measure_text(&text, None, font_size as u16, 1.); + let padding = 4.; + let x = nearby_floor_book.position().x - camera.position.x; + let y = nearby_floor_book.position().y - camera.position.y - 16.; + let w = text_dimensions.width; + let h = text_dimensions.height; + draw_rectangle( + x - padding, + y - text_dimensions.offset_y - padding, + w + padding, + h + padding, + WHITE.with_alpha(0.5), + ); + draw_text(text, x, y, font_size, BLACK.with_alpha(0.5)); + draw_triangle( + Vec2::new(x + 4., y + 5.), + Vec2::new(x + 12., y + 13.), + Vec2::new(x + 20., y + 5.), + WHITE.with_alpha(0.5), + ); + } + + // Render debug view if enabled if debug_view { draw_rectangle(0., 0., 284., 56., BLACK); - draw_text("Library Tech Demo 2026-08-22", 4., 16., 16., WHITE); + draw_text("Library Tech Demo 2026-08-23", 4., 16., 16., WHITE); draw_text(format!("fps {}", get_fps()), 4., 32., 16., WHITE); if let Some(id) = local_player_id - && let Some(player) = game.players.get(&id) + && let Some(player) = game.get_player(&id) { draw_text(format!("id {}", id), 4., 48., 16., WHITE); draw_text( - format!("pos ({:.1}, {:.1})", player.position.x, player.position.y), + format!( + "pos ({:.1}, {:.1})", + player.position().x, + player.position().y + ), 60., 32., 16., @@ -122,8 +192,8 @@ pub(crate) fn follow_local_player( 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[player.sprite_index]; - camera.position = player.position - screen_center + player_sprite.size / 2.; + if let Some((_, player)) = game.players().iter().find(|(id, _)| *id == local_player_id) { + let player_sprite = &spritesheet[player.sprite_index()]; + camera.position = player.position() - screen_center + player_sprite.size / 2.; } } diff --git a/common/Cargo.toml b/common/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] anyhow = "1.0.104" +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/common/assets/books.csv b/common/assets/books.csv @@ -0,0 +1,51 @@ +id,title,author,publish_year +caa6db12-b562-4002-b9c7-4544fd3d7201,To Kill a Mockingbird,Harper Lee,1960 +b599ad7a-5733-4187-bade-034fa3660cb1,Outlander,Diana Gabaldon,1991 +23520cc6-e6a4-4dca-a8bf-2cf59fd6fd8a,The Notebook,Nicholas Sparks,1996 +c81b85c5-8cb6-492d-89f2-649b691fe513,Harry Potter and the Sorcerer's Stone,J.K. Rowling,1997 +fe64c8d9-da9b-440e-bc0f-849a41a6adc8,Harry Potter and the Chamber of Secrets,J.K. Rowling,1998 +c3261fb3-6af8-4fe7-887f-17cdb8158aa9,Harry Potter and the Prisoner of Azkaban,J.K. Rowling,1999 +08c8de1f-e8c9-494e-a248-086238a5591b,Harry Potter and the Goblet of Fire,J.K. Rowling,2000 +b00fd15b-46ba-4518-859d-0e2c56f217cb,Harry Potter and the Order of the Phoenix,J.K. Rowling,2003 +d7cd0773-0dd5-4afb-a993-815f548a2460,Harry Potter and the Half-Blood Prince,J.K. Rowling,2005 +6f3d87f6-ea90-4359-a2de-4b58a4ecdfb9,Pride and Prejudice,Jane Austen,1813 +1af35220-20f1-4509-8705-c71f8c9a2ca1,Gone with the Wind,Margaret Mitchell,1936 +1d6bd1f1-3279-4844-bc1f-dc06526bc752,Little Women,Louisa May Alcott,1868 +d0df6c27-645a-4063-887c-bb4651538e5b,Jane Eyre,Charlotte Brontë,1847 +9a214d7b-8609-4a89-80b8-557c737fc95b,Anne of Green Gables,L.M. Montgomery,1908 +08fc196b-d5dc-444f-b317-9a4e1da34798,The Grapes of Wrath,John Steinbeck,1939 +37e8d69c-3f46-4007-9393-019f9cb59132,The Great Gatsby,F. Scott Fitzgerald,1925 +3e1f7b2c-f667-4d92-931c-8d982da89af0,The Adventures of Tom Sawyer,Mark Twain,1876 +92f8c8d6-4ab4-44f8-92bd-46e5a690303c,And Then There Were None,Agatha Christie,1939 +4d6bf833-6e7d-47b3-980c-2561c381e957,Lonesome Dove,Larry McMurtry,1985 +e292f1ac-ee62-49d8-8d1d-7c68c513ce57,Rebecca,Daphne du Maurier,1938 +991fcd93-3898-4db8-88db-4d431ccbfe15,The Clan of the Cave Bear,Jean M. Auel,1980 +f87b928c-9e1c-4fd4-be31-630a5ffbaa60,The Call of the Wild,Jack London,1903 +29174e0c-df88-49ad-b907-b33611171967,The Count of Monte Cristo,Alexandre Dumas,1846 +adeafa95-dd29-4073-ae6b-0779293b0775,Dragonfly in Amber,Diana Gabaldon,1992 +bba1fb46-93bf-4959-b28e-ea455ae3c9f4,Voyager,Diana Gabaldon,1993 +3ed5dabd-f000-4f69-b857-7a8918af98df,Drums of Autumn,Diana Gabaldon,1996 +9e013003-031e-4ab2-8816-59c60387e99c,The Fiery Cross,Diana Gabaldon,2001 +eedf0e6b-e4e9-43d4-979d-9192ac333dce,A Breath of Snow and Ashes,Diana Gabaldon,2005 +b0812f44-97f0-4728-a214-cd0013809971,An Echo in the Bone,Diana Gabaldon,2009 +fce97c63-7796-4608-b8e3-e836840114ca,Written in My Own Heart's Blood,Diana Gabaldon,2014 +c0c4f725-71a1-4694-b447-f53a1322cb34,Go Tell the Bees That I Am Gone,Diana Gabaldon,2021 +12c576c0-02b6-43df-96ce-efccea73b6f3,The Picture of Dorian Gray,Oscar Wilde,1890 +200d19b5-e771-450e-8828-cc20dd96377e,Harry Potter and the Deathly Hallows,J.K. Rowling,2007 +d0c9b20e-37f8-45ef-8688-425273c3a513,Harry Potter and the Cursed Child,J.K. Rowling,2016 +0bcbd86c-d3f3-438f-a5fd-514c220818e5,The Hobbit,J.R.R. Tolkien,1937 +9443e96c-8216-461c-aeaf-fee7fca0125e,The Fellowship of the Ring,J.R.R. Tolkien,1954 +06365de5-3244-4dde-be00-0e3601439a2b,The Two Towers,J.R.R. Tolkien,1954 +bfbf98ff-d672-4294-8f0d-75bf4c3a90a5,The Return of the King,J.R.R. Tolkien,1955 +68f8e739-a18f-4471-8a6d-497b76b1b697,Charlotte's Web,E.B. White,1952 +8a42b0c6-b3e4-49eb-ba98-90e0b18120bf,The Magician's Nephew,C.S. Lewis,1955 +93a1c3d5-86a3-4654-a26e-1474e4ce0b00,"The Lion, the Witch and the Wardrobe",C.S. Lewis,1950 +9581e7e7-cf17-476d-9500-a885a9397002,The Horse and His Boy,C.S. Lewis,1954 +94e73a2f-a0cb-4d47-825d-a2579b7329cf,Prince Caspian,C.S. Lewis,1951 +70eecad3-aa27-45d2-b240-081d8aedd607,The Voyage of the Dawn Treader,C.S. Lewis,1952 +fb4bedc1-0a79-450b-a80e-72b5b9fdca6d,The Silver Chair,C.S. Lewis,1953 +f2495fc6-392e-43b5-9ce8-126738f77739,The Last Battle,C.S. Lewis,1956 +5feadff4-ae64-49fa-b7c3-91d3ae49a24a,A Tree Grows in Brooklyn,Betty Smith,1943 +1ac8dae5-368f-40a7-b677-ea13272d843f,The Book Thief,Markus Zusak,2005 +071e96dc-cde8-49a5-a595-4f3dc40aa562,The Help,Kathryn Stockett,2009 +45189819-52bf-4ddb-a2a6-c7e39b27f617,1984,George Orwell,1949 diff --git a/common/src/game.rs b/common/src/game.rs @@ -1,19 +1,35 @@ use std::{ collections::HashMap, + fmt::Display, + io::Write, time::{Duration, SystemTime}, }; -use macroquad::math::{Rect, Vec2}; +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; + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct GameState { - pub timestamp: SystemTime, - pub players: HashMap<Uuid, Player>, - pub bookshelves: Vec<Bookshelf>, + timestamp: SystemTime, + players: HashMap<Uuid, Player>, + bookshelves: Vec<Bookshelf>, + floor_books: Vec<FloorBook>, } /// Equivalent to [macroquad::math::Rect::overlaps], except using strict comparison (i.e., `<` and @@ -23,7 +39,7 @@ fn rects_strictly_overlap(a: &Rect, b: &Rect) -> bool { } impl GameState { - pub const MAX_PLAYER_SPEED_PX_S: f32 = 60.; + pub const MAX_PLAYER_SPEED_PX_S: f32 = 75.; pub fn apply(&mut self, action: Action) { match action.kind { @@ -43,13 +59,13 @@ impl GameState { player.position += limited_velocity * dt.as_secs_f32(); // Handle collisions with bookshelves - let player_hitbox = player.hitbox.rect().offset(player.position); + let player_collider = player.collider.rect().offset(player.position); for bookshelf in self.bookshelves.iter() { - // If the player's movement would cause their hitbox to intersect with a - // bookshelf's hitbox, then compute the minimum axis-aligned motion required - // to make them touch but not intersect - let bookshelf_hitbox: Rect = bookshelf.bounds.into(); - let (a, b) = (bookshelf_hitbox, player_hitbox); + // 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 @@ -96,8 +112,8 @@ impl GameState { _ => return new.clone(), }; let interpolated_player = Player { - position: (new_player.position - old_player.position) * t - + old_player.position, + position: (new_player.position() - old_player.position()) * t + + old_player.position(), ..new_player.clone() }; output.players.insert(*new_id, interpolated_player); @@ -108,6 +124,27 @@ impl GameState { } 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 remove_player(&mut self, id: &Uuid) -> Option<Player> { + self.players.remove(id) + } + pub fn update_timestamp(&mut self) { + self.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 { @@ -133,16 +170,28 @@ impl Default for GameState { }, }, ], + 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 { - pub name: String, - pub position: Vec2, - pub sprite_index: SpriteIndex, - hitbox: RectSerde, + name: String, + position: Vec2, + sprite_index: SpriteIndex, + collider: RectSerde, timestamp: SystemTime, } @@ -152,14 +201,26 @@ impl Player { name, position: Vec2::ZERO, sprite_index: SpriteIndex::Meeple, - hitbox: RectSerde::new(0., 0., 43.4, 40.2), + 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 sprite_index(&self) -> SpriteIndex { + self.sprite_index + } + pub fn collider(&self) -> RectSerde { + self.collider + } } #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)] -struct RectSerde { +pub struct RectSerde { x: f32, y: f32, w: f32, @@ -167,10 +228,10 @@ struct RectSerde { } impl RectSerde { - fn new(x: f32, y: f32, w: f32, h: f32) -> Self { + pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self { RectSerde { x, y, w, h } } - fn rect(&self) -> Rect { + pub fn rect(&self) -> Rect { (*self).into() } } @@ -203,6 +264,169 @@ impl Bookshelf { } } +#[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, @@ -240,6 +464,7 @@ pub enum ActionKind { #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] pub enum SpriteIndex { Meeple, + FloorBook, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] diff --git a/common/src/net.rs b/common/src/net.rs @@ -11,6 +11,8 @@ use serde::{Serialize, de::DeserializeOwned}; use crate::codec::Codec; +const MAX_NUM_ATTEMPTS: u8 = 3; + pub fn handle_network_read<T: DeserializeOwned>( mut poll: Poll, stream: Arc<Mutex<TcpStream>>, @@ -27,26 +29,38 @@ pub fn handle_network_read<T: DeserializeOwned>( for event in events.iter() { if event.is_readable() { let mut stream = stream.lock().unwrap(); - match stream.read(&mut buffer) { - Ok(0) => return, - Ok(i) => { - buffer_index = i; - let messages = codec.decode(&buffer[..buffer_index]); - for message in messages { - if let Ok(m) = postcard::from_bytes(message.as_slice()) - && tx.send(m).is_err() - { - break; + let mut all_received = false; + let mut attempts = 0; + while !all_received && attempts < MAX_NUM_ATTEMPTS { + match stream.read(&mut buffer) { + Ok(0) => { + return; + } + Ok(i) => { + buffer_index = i; + let messages = codec.decode(&buffer[..buffer_index]); + for message in messages { + if let Ok(m) = postcard::from_bytes(message.as_slice()) + && tx.send(m).is_err() + { + break; + } } } + Err(e) => match e.kind() { + io::ErrorKind::WouldBlock => { + all_received = true; + } + io::ErrorKind::NotConnected + | io::ErrorKind::ResourceBusy + | io::ErrorKind::Interrupted => { + attempts += 1; + } + _ => { + return; + } + }, } - Err(e) => match e.kind() { - io::ErrorKind::WouldBlock - | io::ErrorKind::NotConnected - | io::ErrorKind::ResourceBusy - | io::ErrorKind::Interrupted => (), - _ => return, - }, } } } @@ -57,22 +71,22 @@ pub fn handle_network_write<T: Serialize>(stream: Arc<Mutex<TcpStream>>, rx: Rec loop { match rx.recv() { Ok(m) => { - if let Ok(to_write) = postcard::to_stdvec(&m) { + if let Ok(to_write) = postcard::to_stdvec(&m) + && let Ok(mut s) = stream.lock() + { let mut all_sent = false; - while !all_sent { - if let Ok(mut s) = stream.lock() { - match s.write_all(Codec::encode(&to_write).as_slice()) { - Ok(_) => all_sent = true, - Err(e) => match e.kind() { - io::ErrorKind::WouldBlock - | io::ErrorKind::NotConnected - | io::ErrorKind::ResourceBusy - | io::ErrorKind::Interrupted => { - all_sent = false; - } - _ => return, - }, - } + let mut attempts = 0; + let bytes = Codec::encode(&to_write); + while !all_sent && attempts < MAX_NUM_ATTEMPTS { + match s.write_all(bytes.as_slice()) { + Ok(_) => all_sent = true, + Err(e) => match e.kind() { + io::ErrorKind::WouldBlock + | io::ErrorKind::NotConnected + | io::ErrorKind::ResourceBusy + | io::ErrorKind::Interrupted => attempts += 1, + _ => return, + }, } } } diff --git a/server/src/main.rs b/server/src/main.rs @@ -2,7 +2,7 @@ use std::{ collections::LinkedList, sync::{Arc, Mutex, mpsc::channel}, thread, - time::{Instant, SystemTime}, + time::Instant, }; use common::game::{GameState, Response, ResponseKind, TICK_DURATION}; @@ -38,7 +38,7 @@ fn main() { // Clean up players associated with clients that have disconnected for dropped_client in c.extract_if(|client| client.is_finished()) { if let Some(id) = dropped_client.id() { - game_state.players.remove(&id); + game_state.remove_player(&id); send_game_state = true; } } @@ -53,7 +53,7 @@ fn main() { // Only bother to send the latest game status to clients if we really need to, instead // of every tick if send_game_state { - game_state.timestamp = SystemTime::now(); + game_state.update_timestamp(); for client in c.iter() { let _ = client.send(Response::new( client.sequence_number(),