diff options
-rw-r--r-- | data/maps/5star.yaml | 95 | ||||
-rw-r--r-- | data/maps/debug.yaml | 1 | ||||
-rw-r--r-- | data/maps/duplex.yaml | 5 | ||||
-rw-r--r-- | data/maps/sushibar.yaml | 32 | ||||
-rw-r--r-- | data/recipes/default.ts | 15 | ||||
-rw-r--r-- | pixel-client/src/game.rs | 16 | ||||
-rw-r--r-- | protocol.md | 2 | ||||
-rw-r--r-- | server/protocol/src/lib.rs | 15 | ||||
-rw-r--r-- | server/protocol/src/movement.rs | 13 | ||||
-rw-r--r-- | server/src/data.rs | 5 | ||||
-rw-r--r-- | server/src/entity/customers/mod.rs | 129 | ||||
-rw-r--r-- | server/src/game.rs | 36 | ||||
-rw-r--r-- | server/src/lib.rs | 3 | ||||
-rw-r--r-- | server/src/main.rs | 15 | ||||
-rw-r--r-- | server/src/state.rs | 59 | ||||
-rw-r--r-- | test-client/main.ts | 20 | ||||
-rw-r--r-- | test-client/protocol.ts | 14 |
17 files changed, 274 insertions, 201 deletions
diff --git a/data/maps/5star.yaml b/data/maps/5star.yaml index 7bd19997..c281cca1 100644 --- a/data/maps/5star.yaml +++ b/data/maps/5star.yaml @@ -1,7 +1,5 @@ # Hurry Curry! - a game about cooking -# Copyright 2024 Sofviic # Copyright 2024 metamuffin -# Copyright 2024 nokoe # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -17,75 +15,85 @@ # score_baseline: 200 map: - - "''''''''''''''''''''''''████████████████████''''''''''''" - - "'''██▒███▒███▒███▒███▒███#LRTF#LRTF#pppppp#█======''''''" - - "'''█ctc.ctc.ctc.ctc..c..█..................d__'*'=''''''" - - "'''█.c...c...c...c..ctc.█..#pppp#fff#CCC..o█X_'''=''''''" - - "'''█c...................d...........#.....o█X_'''=''''''" - - "'''▒tc..............ctc.█#.......~........o█X_''*=''''''" - - "'''█c...c....c.......c..█#pppsss....SSSSSS#█'*'''=''''''" - - "'''█...ctc..ctc...███████████████dd███████████████''''''" - - "'''█c..ctc..ctc...█...ccccccc..█....█..ccccccc...█''''''" - - "'''▒tc.ctc..ctc...█...ttttttt..█....█..ttttttt...▒''''''" - - "'''█c...c....c....█.ct.........d....d.........tc.▒''''''" - - "'''█..............█...ttttttt..█....█..ttttttt...▒''''''" - - "'''█c...c....c....█...ccccccc..█....█..ccccccc...█''''''" - - "'''▒tc.ctc..ctc...██████████████....██████████████''''''" - - "'''█c..ctc..ctc...█...ccccccc..█....█..ccccccc...█''''''" - - "'''█...ctc..ctc...█...ttttttt..█....█..ttttttt...▒''''''" - - "'''█c...c....c....█.ct.........d....d.........tc.▒''''''" - - "'''▒tc............█...ttttttt..█....█..ttttttt...▒''''''" - - "'''█c...c....c....█...ccccccc..█....█..ccccccc...█''''''" - - "'''█...ctc..ctc...██████████████....██████████████''''''" - - "'''█c..ctc..ctc...█...ccccccc..█....█..ccccccc...█''''''" - - "'''▒tc.ctc..ctc...█...ttttttt..█....█..ttttttt...▒''''''" - - "'''█c...c....c....█.ct.........d....d.........tc.▒''''''" - - "'''█.c........c...█...ttttttt..█....█..ttttttt...▒''''''" - - "'''█ctc......ctc..█...ccccccc..█....█..ccccccc...█''''''" - - "'''██▒████dd██▒██████▒▒▒▒▒▒▒▒████dd████▒▒▒▒▒▒▒▒███''''''" - - "''''''''''__'''''''''''''''''''''__'''''''''''''''''''''" - - "___________________________________'''''''''''''''''''''" - - "_!_________________________________'''''''''''''''''''''" - - "''''''''''''''''''''''''''''''''''''''''''''''''''''''''" + - " ''''''''''''' " + - " *''*''''''''*''''*'''*''*' " + - " ''''''''*''''''''*''''''''' " + - "'''██████████████████████''*' " + - "'''█c..........d.......#█'''' " + - "'''▒tc..c...c..d..vv#..#▒'''' " + - " ''█c..ctc.ctc.█..v##..S▒''*' " + - " ''▒...ctc.ctc.w.......S█''*' " + - " ''█c...c...c..w..##C..S█X'' " + - "'''▒tc.........w..##C..#█''' " + - "'''▒tc.........w.....,..d--'' " + - "'''█c...c...c..█s..██dd██'-*' " + - "'''█c..ctc.ctc.█s..█T..F█'-*' " + - " ''▒tc.ctc.ctc.██d██U..G█'-'' " + - " ''█c...c...c..W...█V..R█'-'' " + - " ''▒...........W.##█L..D█'-'' " + - " ''▒.c......c..W.#P██dd██'-'' " + - " ''█ctc....ctc.W.......C█'-'' " + - "'''█ctc....ctc.█.......#█'-'' " + - "'''█.c......c..█ffoo##P#█'-'' " + - "'''██▒███dd██▒██████▒██▒█'-'' " + - "'''''''''--'''''''''''''''-'''" + - "''''Ŧ''''--''''Ŧ'''''''Ŧ''-'''" + - "_____________________________'" + - "_!___________________________'" + - " ______________________,_____'" + - " ''''''''''''''''''''''''''' " + - " ''''''''''''''''''''' " tiles: "#": counter "f": counter - "p": counter - "t": table + "P": counter + "v": counter "w": counter-window + "W": counter-window "s": sink "o": oven "S": stove + "f": freezer "C": cuttingboard + "X": trash + "R": raw-steak-crate + "D": coconut-crate + "V": strawberry-crate + "F": fish-crate + "U": rice-crate "T": tomato-crate - "F": flour-crate + "G": flour-crate "L": leek-crate - "X": trash - "c": chair ".": floor + ",": floor "'": grass + "t": table + "c": chair "*": tree - "~": floor - "!": path - "_": path + "!": street + "_": street + "-": path "d": door "█": wall "▒": wall-window "=": fence + "^": conveyor + "Ŧ": lamp items: "S": pot "w": plate - "p": plate - "f": foodprocessor + "v": plate + "W": glass + "P": foodprocessor entities: - !customers -chef_spawn: "~" +chef_spawn: "," customer_spawn: "!" walkable: @@ -94,6 +102,7 @@ walkable: - chair - grass - path + - street collider: - wall diff --git a/data/maps/debug.yaml b/data/maps/debug.yaml index 93139286..f3472e22 100644 --- a/data/maps/debug.yaml +++ b/data/maps/debug.yaml @@ -123,6 +123,7 @@ walkable: - grass - path - black-hole + - chandelier collider: - wall diff --git a/data/maps/duplex.yaml b/data/maps/duplex.yaml index 59552f09..1265c327 100644 --- a/data/maps/duplex.yaml +++ b/data/maps/duplex.yaml @@ -28,7 +28,7 @@ map: - "''c_c'█S..>›>>>>›#..o█'c_c''" - "''t_t'█S...█''''█...o█'t_t''" - "''c_c'█S...█====█...o█'c_c''" - - "'''_''█#P#F█''''█RTL#█''_'''" + - "'''_''█#PDF█''''█RTL#█''_'''" - "*''_''██████''''██████''_'''" - "'''_''''''''''''''''''''_''*" - "'''______________________'''" @@ -49,7 +49,8 @@ tiles: "o": oven "S": stove "C": cuttingboard - "R": raw-steak-crate + "R": rice-crate + "D": coconut-crate "T": tomato-crate "F": flour-crate "L": leek-crate diff --git a/data/maps/sushibar.yaml b/data/maps/sushibar.yaml index 71bfd968..e086fd37 100644 --- a/data/maps/sushibar.yaml +++ b/data/maps/sushibar.yaml @@ -15,22 +15,22 @@ # score_baseline: 200 map: - - "*''''*''''*''''''''*''" - - "'''*'''''''**''''''''*" - - "''█████████████'X'''*'" - - "''█f.....#c...█'''''**" - - "''█f..⌷⌷.#c...d____'''" - - "'*█o..S⌷.#c...d____'*'" - - "*'█o..S⌷.#c...█''__'''" - - "''█R.....#c...█''__''*" - - "*'█T..⌷⌷.#c.~.█''__'*'" - - "'*█F..C⌷.#c...█''__''*" - - "''█L..C⌷.███d██''__'''" - - "*'█Z..........█''__'*'" - - "'*█Z⌷s⌷ggggg⌷⌷█''__''*" - - "*'█████████████''__'''" - - "'''*'''''''''''''_!'''" - - "'*''''''''''''''''''''" + - "*''''*''''*''''''*''*''" + - "'''*'''''''*'*''''''''*" + - "''██████████████'X'''*'" + - "''█f......#c...█'''''**" + - "''█f..⌷⌷..#c...d____'''" + - "'*█o..S⌷..#c...d____'*'" + - "*'█o..S⌷..#c...█''__'''" + - "''█R......#c...█''__''*" + - "*'█T..⌷⌷..#c.~.█''__'*'" + - "'*█F..C⌷..#c...█''__''*" + - "''█L..C⌷..███d██''__'''" + - "*'█Z...........█''__'*'" + - "'*█Z⌷s⌷gggg⌷⌷⌷⌷█''__''*" + - "''██████████████''__'''" + - "*'''''''''''''''''_!'''" + - "'*''''''''''''''''''''*" tiles: "⌷": counter diff --git a/data/recipes/default.ts b/data/recipes/default.ts index 041b7d99..2fb74c7b 100644 --- a/data/recipes/default.ts +++ b/data/recipes/default.ts @@ -59,7 +59,12 @@ function auto_trash() { if (i instanceof Container) continue if (!i.container) out({ action: "instant", inputs: [i], outputs: [], tile: "trash" }) else { - out({ action: "instant", inputs: [i], outputs: [i.container.dispose ?? i.container], tile: "trash" }) + out({ + action: "instant", + inputs: [i], + outputs: [i.container.dispose ?? i.container], + tile: i.container.dispose_tile ?? "trash" + }) } } } @@ -81,11 +86,11 @@ class Item { } } -class Container extends Item { constructor(name: string, public dispose?: Item) { super(name) } } +class Container extends Item { constructor(name: string, public dispose?: Item, public dispose_tile?: string) { super(name) } } const FP = new Container("foodprocessor") const POT = new Container("pot") const PL = new Container("plate", new Container("dirty-plate")) -const GL = new Container("glass") +const GL = new Container("glass", undefined, "sink") function crate(s: string): Item { const item = new Item(s); @@ -228,10 +233,12 @@ edible(strawberry_mochi) // Drinks edible( strawberry_shake.tr(GL), + tomato_juice.tr(GL), sink_fill(GL) ) -const curry_with_rice = combine(PL, cook(rice.tr(POT)), cook(combine(POT, milk, tomato, leek))) +// Curry +const curry_with_rice = combine(PL, cook(rice.tr(POT)), cook(combine(POT, milk, tomato, leek)).as("curry")) edible(curry_with_rice) auto_trash() diff --git a/pixel-client/src/game.rs b/pixel-client/src/game.rs index af387b95..600ea2f1 100644 --- a/pixel-client/src/game.rs +++ b/pixel-client/src/game.rs @@ -147,23 +147,27 @@ impl Game { if interact != self.interacting { if interact { self.network.queue_out.push_back(PacketS::Interact { + player: self.my_id, pos: Some(self.players[&self.my_id].movement.get_interact_target()), }); } else { - self.network - .queue_out - .push_back(PacketS::Interact { pos: None }); + self.network.queue_out.push_back(PacketS::Interact { + player: self.my_id, + pos: None, + }); } self.interacting = interact; } if let Some(player) = self.players.get_mut(&self.my_id) { - let movement_packet = player + player .movement .update(&self.collision_map, direction, boost, dt); if send_movement { - self.network.queue_out.push_back(movement_packet); + self.network + .queue_out + .push_back(player.movement.movement_packet(direction, self.my_id)); } player.interact_target_anim.exp_to( @@ -205,7 +209,7 @@ impl Game { pub fn packet_in(&mut self, packet: PacketC, layout: &AtlasLayout) { match packet { - PacketC::Init { id } => self.my_id = id, + PacketC::Joined { id } => self.my_id = id, PacketC::Data { data } => { self.tilemap.init(&data.tile_names, layout); self.item_sprites = data diff --git a/protocol.md b/protocol.md index 956663ba..b5201abd 100644 --- a/protocol.md +++ b/protocol.md @@ -23,7 +23,7 @@ The protocol schema is defined in [`protocol.ts`](./test-client/protocol.ts) 1. Connect to the server via WebSocket (on port 27032 for plain HTTP or 443 with SSL) and send/receive json in WebSocket "Text" messages. The binary protocol uses "Binary" messages and is optional for servers and clients. -2. Wait for `init` packet and check version compatibiliy (see below). +2. Wait for `version` packet and check version compatibiliy (see below). 3. Send the join packet with your username. 4. The server will send the current game state: - `data` once for setting important look-up tables diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs index 02c6d0b1..a56b6edb 100644 --- a/server/protocol/src/lib.rs +++ b/server/protocol/src/lib.rs @@ -88,8 +88,11 @@ pub enum PacketS { name: String, character: i32, }, - Leave, + Leave { + player: PlayerID, + }, Movement { + player: PlayerID, #[bincode(with_serde)] direction: Vec2, boosting: bool, @@ -97,15 +100,12 @@ pub enum PacketS { pos: Option<Vec2>, }, Interact { - #[bincode(with_serde)] - pos: Option<IVec2>, - }, - Collide { player: PlayerID, #[bincode(with_serde)] - force: Vec2, + pos: Option<IVec2>, }, Communicate { + player: PlayerID, message: Option<Message>, persist: bool, }, @@ -114,6 +114,7 @@ pub enum PacketS { #[bincode(skip)] /// For internal use only ReplaceHand { + player: PlayerID, item: Option<ItemIndex>, }, /// For use in replay sessions only @@ -138,7 +139,7 @@ pub enum PacketC { major: u32, supports_bincode: bool, }, - Init { + Joined { id: PlayerID, }, Data { diff --git a/server/protocol/src/movement.rs b/server/protocol/src/movement.rs index 286c7f6a..5525c5e6 100644 --- a/server/protocol/src/movement.rs +++ b/server/protocol/src/movement.rs @@ -17,7 +17,7 @@ */ use crate::{ glam::{IVec2, Vec2}, - PacketS, + PacketS, PlayerID, }; use std::collections::HashSet; @@ -48,13 +48,7 @@ impl MovementBase { rotation: 0., } } - pub fn update( - &mut self, - map: &HashSet<IVec2>, - direction: Vec2, - mut boost: bool, - dt: f32, - ) -> PacketS { + pub fn update(&mut self, map: &HashSet<IVec2>, direction: Vec2, mut boost: bool, dt: f32) { let direction = direction.clamp_length_max(1.); if direction.length() > 0.1 { self.facing = direction + (self.facing - direction) * (-dt * 10.).exp(); @@ -73,11 +67,14 @@ impl MovementBase { self.position += self.velocity * dt; self.velocity *= (-dt * PLAYER_FRICTION).exp(); collide_player_tiles(self, map); + } + pub fn movement_packet(&self, direction: Vec2, player: PlayerID) -> PacketS { PacketS::Movement { pos: Some(self.position), boosting: self.boosting, direction, + player, } } diff --git a/server/src/data.rs b/server/src/data.rs index 48540d72..522df916 100644 --- a/server/src/data.rs +++ b/server/src/data.rs @@ -238,7 +238,10 @@ impl Gamedata { let mut tiles_used = HashSet::new(); let mut items_used = HashSet::new(); for (y, line) in map_in.map.iter().enumerate() { - for (x, tile) in line.trim().chars().enumerate() { + for (x, tile) in line.chars().enumerate() { + if tile == ' ' { + continue; // space is empty space + } let pos = IVec2::new(x as i32, y as i32); if tile == map_in.chef_spawn { chef_spawn = pos.as_vec2() + Vec2::splat(0.5); diff --git a/server/src/entity/customers/mod.rs b/server/src/entity/customers/mod.rs index b5b9fa42..e6067110 100644 --- a/server/src/entity/customers/mod.rs +++ b/server/src/entity/customers/mod.rs @@ -31,7 +31,7 @@ use std::collections::{HashMap, VecDeque}; #[derive(Debug, Clone)] pub struct Customers { demands: Vec<Demand>, - cpackets: VecDeque<(PlayerID, PacketS)>, + cpackets: VecDeque<PacketS>, chairs: HashMap<IVec2, bool>, customer_id_counter: PlayerID, customers: HashMap<PlayerID, CustomerState>, @@ -84,13 +84,10 @@ impl EntityT for Customers { self.spawn_cooldown = 10. + random::<f32>() * 10.; self.customer_id_counter.0 -= 1; let id = self.customer_id_counter; - self.cpackets.push_back(( - id, - PacketS::Join { - name: faker::name::fr_fr::Name().fake(), - character: -1 - (random::<u16>() as i32), - }, - )); + self.cpackets.push_back(PacketS::Join { + name: faker::name::fr_fr::Name().fake(), + character: -1 - (random::<u16>() as i32), + }); let chair = self.select_chair().ok_or(anyhow!("no free chair found"))?; let from = game.data.customer_spawn.as_ivec2(); let path = find_path(&game.walkable, from, chair) @@ -100,24 +97,22 @@ impl EntityT for Customers { .insert(id, CustomerState::Entering { path, chair }); } let mut customers_to_remove = Vec::new(); - for (&id, state) in &mut self.customers { - let Some(player) = game.players.get_mut(&id) else { + for (&player, state) in &mut self.customers { + let Some(playerdata) = game.players.get_mut(&player) else { continue; }; match state { CustomerState::Entering { path, chair } => { - player.direction = path.next_direction(player.position()); + playerdata.direction = path.next_direction(playerdata.position()); if path.is_done() { let demand = DemandIndex(random::<usize>() % self.demands.len()); - self.cpackets.push_back(( - id, - PacketS::Communicate { - message: Some(Message::Item(self.demands[demand.0].from)), - persist: true, - }, - )); - info!("{id:?} -> waiting"); + self.cpackets.push_back(PacketS::Communicate { + message: Some(Message::Item(self.demands[demand.0].from)), + persist: true, + player, + }); + info!("{player:?} -> waiting"); *state = CustomerState::Waiting { chair: *chair, timeout: 90. + random::<f32>() * 60., @@ -130,26 +125,22 @@ impl EntityT for Customers { demand, timeout, } => { - player.direction = (chair.as_vec2() + 0.5) - player.position(); + playerdata.direction = (chair.as_vec2() + 0.5) - playerdata.position(); *timeout -= dt; if *timeout <= 0. { - self.cpackets.push_back(( - id, - PacketS::Communicate { - message: None, - persist: true, - }, - )); - self.cpackets.push_back(( - id, - PacketS::Communicate { - message: Some(Message::Effect("angry".to_string())), - persist: false, - }, - )); + self.cpackets.push_back(PacketS::Communicate { + message: None, + persist: true, + player, + }); + self.cpackets.push_back(PacketS::Communicate { + message: Some(Message::Effect("angry".to_string())), + persist: false, + player, + }); let path = find_path( &game.walkable, - player.position().as_ivec2(), + playerdata.position().as_ivec2(), game.data.customer_spawn.as_ivec2(), ) .expect("no path to exit"); @@ -157,7 +148,7 @@ impl EntityT for Customers { game.score.demands_failed += 1; game.score.points -= 1; game.score_changed = true; - info!("{id:?} -> exiting"); + info!("{player:?} -> exiting"); *state = CustomerState::Exiting { path } } else { let demand_data = &self.demands[demand.0]; @@ -182,25 +173,23 @@ impl EntityT for Customers { } }); if let Some(pos) = demand_pos { - self.cpackets.push_back(( - id, - PacketS::Communicate { - persist: true, - message: None, - }, - )); - self.cpackets.push_back(( - id, - PacketS::Communicate { - message: Some(Message::Effect("satisfied".to_string())), - persist: false, - }, - )); - self.cpackets - .push_back((id, PacketS::Interact { pos: Some(pos) })); + self.cpackets.push_back(PacketS::Communicate { + persist: true, + message: None, + player, + }); + self.cpackets.push_back(PacketS::Communicate { + message: Some(Message::Effect("satisfied".to_string())), + persist: false, + player, + }); + self.cpackets.push_back(PacketS::Interact { + pos: Some(pos), + player, + }); self.cpackets - .push_back((id, PacketS::Interact { pos: None })); - info!("{id:?} -> eating"); + .push_back(PacketS::Interact { pos: None, player }); + info!("{player:?} -> eating"); *state = CustomerState::Eating { demand: *demand, target: pos, @@ -216,21 +205,25 @@ impl EntityT for Customers { progress, chair, } => { - player.direction = (chair.as_vec2() + 0.5) - player.position(); + playerdata.direction = (chair.as_vec2() + 0.5) - playerdata.position(); let demand = &self.demands[demand.0]; *progress += dt / demand.duration; if *progress >= 1. { - self.cpackets - .push_back((id, PacketS::ReplaceHand { item: demand.to })); + self.cpackets.push_back(PacketS::ReplaceHand { + player, + item: demand.to, + }); if demand.to.is_some() { + self.cpackets.push_back(PacketS::Interact { + player, + pos: Some(*target), + }); self.cpackets - .push_back((id, PacketS::Interact { pos: Some(*target) })); - self.cpackets - .push_back((id, PacketS::Interact { pos: None })); + .push_back(PacketS::Interact { player, pos: None }); } let path = find_path( &game.walkable, - player.position().as_ivec2(), + playerdata.position().as_ivec2(), game.data.customer_spawn.as_ivec2(), ) .ok_or(anyhow!("no path to exit"))?; @@ -238,16 +231,16 @@ impl EntityT for Customers { game.score.demands_completed += 1; game.score.points += demand.points; game.score_changed = true; - info!("{id:?} -> exiting"); + info!("{player:?} -> exiting"); *state = CustomerState::Exiting { path } } } CustomerState::Exiting { path } => { - player.direction = path.next_direction(player.position()); + playerdata.direction = path.next_direction(playerdata.position()); if path.is_done() { - info!("{id:?} -> leave"); - self.cpackets.push_back((id, PacketS::Leave)); - customers_to_remove.push(id); + info!("{player:?} -> leave"); + self.cpackets.push_back(PacketS::Leave { player }); + customers_to_remove.push(player); } } } @@ -255,8 +248,8 @@ impl EntityT for Customers { for c in customers_to_remove { self.customers.remove(&c).unwrap(); } - for (player, packet) in self.cpackets.drain(..) { - if let Err(err) = game.packet_in(player, packet, &mut vec![], packet_out) { + for packet in self.cpackets.drain(..) { + if let Err(err) = game.packet_in(packet, &mut vec![], packet_out) { warn!("demand packet {err}"); } } diff --git a/server/src/game.rs b/server/src/game.rs index e0154c4c..6477d9fa 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -79,6 +79,8 @@ pub struct Game { pub environment_effects: HashSet<String>, pub score_changed: bool, pub score: Score, + + pub player_id_counter: PlayerID, } impl Default for Game { @@ -101,6 +103,7 @@ impl Game { score: Score::default(), environment_effects: HashSet::default(), score_changed: false, + player_id_counter: PlayerID(1), } } @@ -272,23 +275,24 @@ impl Game { pub fn packet_in( &mut self, - player: PlayerID, packet: PacketS, replies: &mut Vec<PacketC>, packet_out: &mut VecDeque<PacketC>, ) -> Result<()> { match packet { PacketS::Join { name, character } => { - if self.players.contains_key(&player) { + let id = self.player_id_counter; + self.player_id_counter.0 += 1; + if self.players.contains_key(&id) { bail!("You already joined.") } - let position = if player.0 < 0 { + let position = if id.0 < 0 { self.data.customer_spawn } else { self.data.chef_spawn }; self.players.insert( - player, + id, Player { item: None, character, @@ -314,13 +318,14 @@ impl Game { ); self.score.players = self.score.players.max(self.players.len()); packet_out.push_back(PacketC::AddPlayer { - id: player, + id, name, position, character, }); + replies.push(PacketC::Joined { id }) } - PacketS::Leave => { + PacketS::Leave { player } => { let p = self .players .remove(&player) @@ -346,6 +351,7 @@ impl Game { pos, boosting, direction, + player, } => { let player = self .players @@ -366,10 +372,7 @@ impl Game { } } } - PacketS::Collide { player, force } => { - packet_out.push_back(PacketC::Collide { player, force }); - } - PacketS::Interact { pos } => { + PacketS::Interact { pos, player } => { let pid = player; let player = self .players @@ -450,7 +453,11 @@ impl Game { ) } } - PacketS::Communicate { message, persist } => { + PacketS::Communicate { + message, + persist, + player, + } => { info!("{player:?} message {message:?}"); if persist { if let Some(player) = self.players.get_mut(&player) { @@ -463,7 +470,7 @@ impl Game { persist, }) } - PacketS::ReplaceHand { item } => { + PacketS::ReplaceHand { item, player } => { let pdata = self .players .get_mut(&player) @@ -592,10 +599,9 @@ impl Game { } } } - for pid in players_auto_release.drain(..) { + for player in players_auto_release.drain(..) { let _ = self.packet_in( - pid, - PacketS::Interact { pos: None }, + PacketS::Interact { pos: None, player }, &mut vec![], packet_out, ); diff --git a/server/src/lib.rs b/server/src/lib.rs index 2cbcc10b..c8f7af8c 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -25,6 +25,9 @@ pub mod state; use hurrycurry_protocol::glam::Vec2; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ConnectionID(pub i64); + pub trait InterpolateExt { fn exp_to(&mut self, target: Self, dt: f32); } diff --git a/server/src/main.rs b/server/src/main.rs index f43f668b..0f38aa2b 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -18,8 +18,8 @@ use anyhow::{anyhow, Result}; use clap::Parser; use futures_util::{SinkExt, StreamExt}; -use hurrycurry_protocol::{PacketC, PacketS, PlayerID, BINCODE_CONFIG, VERSION}; -use hurrycurry_server::{data::DATA_DIR, state::State}; +use hurrycurry_protocol::{PacketC, PacketS, BINCODE_CONFIG, VERSION}; +use hurrycurry_server::{data::DATA_DIR, state::State, ConnectionID}; use log::{debug, info, trace, warn, LevelFilter}; use std::{ net::SocketAddr, @@ -55,7 +55,7 @@ struct Args { fn main() -> Result<()> { env_logger::builder() - .filter_level(LevelFilter::Info) + .filter_level(LevelFilter::Warn) .parse_env("LOG") .init(); @@ -115,7 +115,7 @@ async fn run(addr: SocketAddr) -> anyhow::Result<()> { }); } - for id in (1..).map(PlayerID) { + for id in (1..).map(ConnectionID) { let (sock, addr) = ws_listener.accept().await?; let Ok(sock) = tokio_tungstenite::accept_async(sock).await else { warn!("invalid ws handshake"); @@ -137,7 +137,6 @@ async fn run(addr: SocketAddr) -> anyhow::Result<()> { supports_bincode: true, }, ); - init.insert(1, PacketC::Init { id }); let supports_binary = Arc::new(AtomicBool::new(false)); let supports_binary2 = supports_binary.clone(); @@ -185,7 +184,7 @@ async fn run(addr: SocketAddr) -> anyhow::Result<()> { }); spawn(async move { - info!("{id:?} joined"); + info!("{id:?} connected"); while let Some(Ok(message)) = read.next().await { let packet = match message { Message::Text(line) => match serde_json::from_str(&line) { @@ -230,8 +229,8 @@ async fn run(addr: SocketAddr) -> anyhow::Result<()> { let _ = error_tx.send(packet).await; } } - info!("{id:?} left"); - state.write().await.packet_in(id, PacketS::Leave).await.ok(); + info!("{id:?} disconnected"); + let _ = state.write().await.disconnect(id).await; }); } Ok(()) diff --git a/server/src/state.rs b/server/src/state.rs index 97261fab..43ca29bd 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -15,18 +15,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use crate::{data::DataIndex, game::Game}; +use crate::{data::DataIndex, game::Game, ConnectionID}; use anyhow::{anyhow, bail, Result}; use clap::{Parser, ValueEnum}; use hurrycurry_protocol::{Message, PacketC, PacketS, PlayerID}; use log::{debug, trace}; -use std::{collections::VecDeque, time::Duration}; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + time::Duration, +}; use tokio::sync::broadcast::Sender; pub struct State { index: DataIndex, packet_out: VecDeque<PacketC>, tx: Sender<PacketC>, + connections: HashMap<ConnectionID, HashSet<PlayerID>>, pub game: Game, } @@ -89,6 +93,7 @@ impl State { index, tx, packet_out, + connections: HashMap::new(), }) } @@ -110,14 +115,20 @@ impl State { } Ok(()) } - pub async fn packet_in(&mut self, player: PlayerID, packet: PacketS) -> Result<Vec<PacketC>> { + pub async fn packet_in(&mut self, conn: ConnectionID, packet: PacketS) -> Result<Vec<PacketC>> { + if let Some(p) = get_packet_player(&packet) { + if !self.connections.entry(conn).or_default().contains(&p) { + bail!("Packet sent to player that is not owned by this connection."); + } + } let mut replies = Vec::new(); match &packet { PacketS::Communicate { message: Some(Message::Text(text)), persist: false, + player, } if let Some(command) = text.strip_prefix("/") => { - match self.handle_command_parse(player, command).await { + match self.handle_command_parse(*player, command).await { Ok(()) => return Ok(vec![]), Err(e) => { return Ok(vec![PacketC::ServerMessage { @@ -126,10 +137,27 @@ impl State { } } } + PacketS::Leave { player } => { + self.connections.entry(conn).or_default().remove(player); + } + PacketS::Join { .. } => { + if self.connections.entry(conn).or_default().len() > 8 { + bail!("Players per connection limit exceeded.") + } + } _ => (), } self.game - .packet_in(player, packet, &mut replies, &mut self.packet_out)?; + .packet_in(packet, &mut replies, &mut self.packet_out)?; + + for p in &replies { + match p { + PacketC::Joined { id } => { + self.connections.entry(conn).or_default().insert(*id); + } + _ => (), + } + } if self.game.count_chefs() <= 0 && !self.game.lobby { self.tx @@ -146,6 +174,15 @@ impl State { Ok(replies) } + pub async fn disconnect(&mut self, conn: ConnectionID) { + if let Some(players) = self.connections.get(&conn) { + for player in players.to_owned() { + let _ = self.packet_in(conn, PacketS::Leave { player }).await; + } + } + self.connections.remove(&conn); + } + async fn handle_command_parse(&mut self, player: PlayerID, command: &str) -> Result<()> { self.handle_command( player, @@ -239,3 +276,15 @@ impl State { Ok(()) } } + +fn get_packet_player(packet: &PacketS) -> Option<PlayerID> { + match packet { + PacketS::Join { .. } => None, + PacketS::Leave { player } => Some(*player), + PacketS::Movement { player, .. } => Some(*player), + PacketS::Interact { player, .. } => Some(*player), + PacketS::Communicate { player, .. } => Some(*player), + PacketS::ReplaceHand { player, .. } => Some(*player), + PacketS::ReplayTick { .. } => None, + } +} diff --git a/test-client/main.ts b/test-client/main.ts index e1313f8c..f0f481b4 100644 --- a/test-client/main.ts +++ b/test-client/main.ts @@ -131,7 +131,7 @@ function packet(p: PacketC) { case "version": console.log(`Protocol version: ${p.major}.${p.minor}`); break; - case "init": + case "joined": my_id = p.id break; case "data": @@ -257,12 +257,12 @@ function keyboard(ev: KeyboardEvent, down: boolean) { if (HANDLED_KEYS.includes(ev.code)) ev.preventDefault() if (!keys_down.has(KEY_INTERACT) && ev.code == KEY_INTERACT && down) set_interact(true) if (keys_down.has(KEY_INTERACT) && ev.code == KEY_INTERACT && !down) set_interact(false) - if (down && ev.code == "Numpad1") send({ type: "communicate", message: { text: "/start junior" }, persist: false }) - if (down && ev.code == "Numpad2") send({ type: "communicate", message: { text: "/start senior" }, persist: false }) - if (down && ev.code == "Numpad3") send({ type: "communicate", message: { text: "/start sophomore" }, persist: false }) - if (down && ev.code == "Numpad4") send({ type: "communicate", message: { text: "/start debug" }, persist: false }) - if (down && ev.code == "Numpad5") send({ type: "communicate", message: { text: "/start bus" }, persist: false }) - if (down && ev.code == "Numpad0") send({ type: "communicate", message: { text: "/end" }, persist: false }) + if (down && ev.code == "Numpad1") send({ player: my_id, type: "communicate", message: { text: "/start junior" }, persist: false }) + if (down && ev.code == "Numpad2") send({ player: my_id, type: "communicate", message: { text: "/start senior" }, persist: false }) + if (down && ev.code == "Numpad3") send({ player: my_id, type: "communicate", message: { text: "/start sophomore" }, persist: false }) + if (down && ev.code == "Numpad4") send({ player: my_id, type: "communicate", message: { text: "/start debug" }, persist: false }) + if (down && ev.code == "Numpad5") send({ player: my_id, type: "communicate", message: { text: "/start bus" }, persist: false }) + if (down && ev.code == "Numpad0") send({ player: my_id, type: "communicate", message: { text: "/end" }, persist: false }) if (down) keys_down.add(ev.code) else keys_down.delete(ev.code) } @@ -275,7 +275,7 @@ function close_chat() { } function toggle_chat() { if (chat) { - if (chat.value.length) send({ type: "communicate", message: { text: chat.value }, persist: false }) + if (chat.value.length) send({ player: my_id, type: "communicate", message: { text: chat.value }, persist: false }) chat.remove() canvas.focus() chat = null; @@ -300,14 +300,14 @@ export function get_interact_target(): V2 | undefined { function set_interact(edge: boolean) { if (edge) interacting = get_interact_target() - if (interacting) send({ type: "interact", pos: edge ? [interacting.x, interacting.y] : undefined }) + if (interacting) send({ player: my_id, type: "interact", pos: edge ? [interacting.x, interacting.y] : undefined }) if (!edge) interacting = undefined } function tick_update() { const p = players.get(my_id) if (!p) return - send({ type: "movement", pos: [p.position.x, p.position.y], direction: [p.direction.x, p.direction.y], boosting: p.boosting }) + send({ player: my_id, type: "movement", pos: [p.position.x, p.position.y], direction: [p.direction.x, p.direction.y], boosting: p.boosting }) } function frame_update(dt: number) { diff --git a/test-client/protocol.ts b/test-client/protocol.ts index bd32e497..353c9100 100644 --- a/test-client/protocol.ts +++ b/test-client/protocol.ts @@ -36,16 +36,16 @@ export interface Gamedata { } export type PacketS = - { type: "join", name: string, character: number } // Spawns your character. Dont send it to spectate. - | { type: "leave" } // Despawns your character - | { type: "movement", pos: Vec2, direction: Vec2, boosting: boolean } - | { type: "interact", pos?: Vec2 } // Interact with some tile. pos is a position when pressing and null when releasing interact button - | { type: "communicate", message?: Message, persist: boolean } // Send a message - | { type: "collide", player: PlayerID, force: Vec2 } // Apply force to another player as a result of a collision + { type: "join", name: string, character: number } // Spawns a new character. The server replies with "joined". Dont send it to spectate. + | { type: "leave", player: PlayerID } // Despawns a character + | { type: "movement", player: PlayerID, pos: Vec2, direction: Vec2, boosting: boolean } + | { type: "interact", player: PlayerID, pos?: Vec2 } // Interact with some tile. pos is a position when pressing and null when releasing interact button + | { type: "communicate", player: PlayerID, message?: Message, persist: boolean } // Send a message + | { type: "replay_tick", dt: number } // Steps forward in replay. export type PacketC = { type: "version", minor: number, major: number, supports_bincode?: boolean } // Sent once after connecting to ensure you client is compatible - | { type: "init", id: PlayerID } // You just connected. This is your id for this session. + | { type: "joined", id: PlayerID } // Informs you about the id of the character you spawned | { type: "data", data: Gamedata } // Game data was changed | { type: "add_player", id: PlayerID, name: string, position: Vec2, character: number } // Somebody else joined (or was already in the game) | { type: "remove_player", id: PlayerID } // Somebody left |