aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/maps/5star.yaml95
-rw-r--r--data/maps/debug.yaml1
-rw-r--r--data/maps/duplex.yaml5
-rw-r--r--data/maps/sushibar.yaml32
-rw-r--r--data/recipes/default.ts15
-rw-r--r--pixel-client/src/game.rs16
-rw-r--r--protocol.md2
-rw-r--r--server/protocol/src/lib.rs15
-rw-r--r--server/protocol/src/movement.rs13
-rw-r--r--server/src/data.rs5
-rw-r--r--server/src/entity/customers/mod.rs129
-rw-r--r--server/src/game.rs36
-rw-r--r--server/src/lib.rs3
-rw-r--r--server/src/main.rs15
-rw-r--r--server/src/state.rs59
-rw-r--r--test-client/main.ts20
-rw-r--r--test-client/protocol.ts14
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