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:
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(),