aboutsummaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/Cargo.toml2
-rw-r--r--server/bot/Cargo.toml3
-rw-r--r--server/bot/src/algos/customer.rs278
-rw-r--r--server/bot/src/algos/dishwasher.rs63
-rw-r--r--server/bot/src/algos/frank.rs75
-rw-r--r--server/bot/src/algos/mod.rs18
-rw-r--r--server/bot/src/algos/simple.rs76
-rw-r--r--server/bot/src/algos/test.rs67
-rw-r--r--server/bot/src/algos/waiter.rs59
-rw-r--r--server/bot/src/lib.rs38
-rw-r--r--server/bot/src/main.rs33
-rw-r--r--server/bot/src/pathfinding.rs4
-rw-r--r--server/bot/src/step.rs121
-rw-r--r--server/data/src/lib.rs13
-rw-r--r--server/protocol/src/helpers.rs101
-rw-r--r--server/protocol/src/lib.rs76
-rw-r--r--server/src/commands.rs3
-rw-r--r--server/src/entity/bot.rs106
-rw-r--r--server/src/entity/customers.rs6
-rw-r--r--server/src/server.rs1
-rw-r--r--server/src/state.rs23
21 files changed, 584 insertions, 582 deletions
diff --git a/server/Cargo.toml b/server/Cargo.toml
index b6da3738..6da63fcb 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -40,5 +40,5 @@ default = ["mdns", "register", "upnp"]
mdns = ["dep:mdns-sd", "dep:get_if_addrs"]
register = ["dep:reqwest"]
upnp = ["dep:igd", "dep:get_if_addrs"]
-
+debug_events = ["hurrycurry-bot/debug_events"]
fast_recipes = ["hurrycurry-data/fast_recipes"]
diff --git a/server/bot/Cargo.toml b/server/bot/Cargo.toml
index ad63f709..8a841843 100644
--- a/server/bot/Cargo.toml
+++ b/server/bot/Cargo.toml
@@ -11,3 +11,6 @@ anyhow = "1.0.99"
env_logger = "0.11.8"
rustls = { version = "0.23.31", features = ["ring"] }
clap = { version = "4.5.47", features = ["derive"] }
+
+[features]
+debug_events = []
diff --git a/server/bot/src/algos/customer.rs b/server/bot/src/algos/customer.rs
index 5a6f71f7..6ae67f69 100644
--- a/server/bot/src/algos/customer.rs
+++ b/server/bot/src/algos/customer.rs
@@ -1,5 +1,3 @@
-use std::random::random;
-
/*
Hurry Curry! - a game about cooking
Copyright (C) 2025 Hurry Curry! Contributors
@@ -18,7 +16,7 @@ use std::random::random;
*/
use crate::{
- BotAlgo, BotInput,
+ BotAlgo, PacketSink,
pathfinding::{Path, find_path},
random_float,
};
@@ -28,10 +26,11 @@ use hurrycurry_protocol::{
glam::{IVec2, Vec2},
};
use log::debug;
+use std::random::random;
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone)]
pub struct Customer {
- config: CustomerConfig,
+ me: PlayerID,
state: CustomerState,
}
@@ -74,34 +73,31 @@ enum CustomerState {
Exiting {
path: Path,
},
+ Exited,
}
impl Customer {
- pub fn new(config: CustomerConfig) -> Self {
+ pub fn new(me: PlayerID) -> Self {
Customer {
- config,
+ me,
state: CustomerState::default(),
}
}
}
impl BotAlgo for Customer {
- fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput {
- let Some(playerdata) = game.players.get(&me) else {
- return BotInput::default();
- };
- let pos = playerdata.movement.position;
- self.state.tick(me, &self.config, pos, game, dt)
+ fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) {
+ self.state.tick(&mut out, self.me, game, dt);
+ }
+ fn is_finished(&self) -> bool {
+ matches!(self.state, CustomerState::Exited)
}
}
impl CustomerState {
- fn tick(
- &mut self,
- me: PlayerID,
- config: &CustomerConfig,
- pos: Vec2,
- game: &Game,
- dt: f32,
- ) -> BotInput {
+ fn tick(&mut self, out: &mut PacketSink, me: PlayerID, game: &Game, dt: f32) {
+ let Some(playerdata) = game.players.get(&me) else {
+ return;
+ };
+ let pos = playerdata.movement.position;
match self {
CustomerState::New => {
if !game.data.demands.is_empty() {
@@ -123,7 +119,6 @@ impl CustomerState {
};
}
}
- BotInput::default()
}
CustomerState::Entering {
path,
@@ -158,22 +153,19 @@ impl CustomerState {
check: 0,
pinned: false,
};
- let message_item = if config.unknown_order {
+ let message_item = if !game.data.flags.disable_unknown_orders {
game.data
.get_item_by_name("unknown-order")
.unwrap_or(requested_item)
} else {
requested_item
};
- BotInput {
- extra: vec![PacketS::Communicate {
- message: Some(Message::Item(message_item)),
- timeout: Some(timeout),
- player: me,
- pin: Some(false),
- }],
- ..Default::default()
- }
+ out.push(PacketS::Communicate {
+ message: Some(Message::Item(message_item)),
+ timeout: Some(timeout),
+ player: me,
+ pin: Some(false),
+ });
} else if check
&& path.remaining_segments() < 5
&& game.players.iter().any(|(id, p)| {
@@ -183,17 +175,17 @@ impl CustomerState {
})
{
*self = CustomerState::New;
- BotInput::default()
} else if path.is_stuck() {
if let Some(path) = find_path(game, pos.as_ivec2(), *origin) {
*self = CustomerState::Exiting { path };
}
- BotInput::default()
} else {
- BotInput {
- direction: path.next_direction(pos, dt) * 0.6,
- ..Default::default()
- }
+ out.push(PacketS::Movement {
+ player: me,
+ dir: path.next_direction(pos, dt) * 0.6,
+ boost: false,
+ pos: None,
+ });
}
}
CustomerState::Waiting {
@@ -210,34 +202,30 @@ impl CustomerState {
if *timeout <= 0. {
let path = find_path(game, pos.as_ivec2(), *origin).expect("no path to exit");
debug!("{me:?} -> exiting");
- *self = CustomerState::Exiting { path };
- return BotInput {
- extra: vec![
- PacketS::Communicate {
- message: None,
- timeout: Some(0.),
- player: me,
- pin: Some(false),
- },
- PacketS::ApplyScore(Score {
- points: -1,
- demands_failed: 1,
- ..Default::default()
- }),
- PacketS::Effect {
- name: "angry".to_string(),
- player: me,
- },
- ],
+ out.push(PacketS::Communicate {
+ message: None,
+ timeout: Some(0.),
+ player: me,
+ pin: Some(false),
+ });
+ out.push(PacketS::ApplyScore(Score {
+ points: -1,
+ demands_failed: 1,
..Default::default()
- };
+ }));
+ out.push(PacketS::Effect {
+ name: "angry".to_string(),
+ player: me,
+ });
+ *self = CustomerState::Exiting { path };
+ return;
} else if *check > 10 {
let demand_data = &game.data.demands[demand.0];
*check = 0;
if !*pinned {
let mut pin = false;
- if config.unknown_order {
+ if !game.data.flags.disable_unknown_orders {
game.players_spatial_index.query(pos, 3., |pid, _| {
if game
.players
@@ -252,15 +240,12 @@ impl CustomerState {
}
if pin {
*pinned = true;
- return BotInput {
- extra: vec![PacketS::Communicate {
- player: me,
- message: Some(Message::Item(demand_data.input)),
- timeout: Some(*timeout),
- pin: Some(true),
- }],
- ..Default::default()
- };
+ out.push(PacketS::Communicate {
+ player: me,
+ message: Some(Message::Item(demand_data.input)),
+ timeout: Some(*timeout),
+ pin: Some(true),
+ });
}
}
@@ -287,6 +272,37 @@ impl CustomerState {
if let Some(pos) = demand_pos {
debug!("{me:?} -> eating");
let points = game.data.demands[demand.0].points;
+ out.push(PacketS::Communicate {
+ message: None,
+ timeout: Some(0.),
+ player: me,
+ pin: Some(false),
+ });
+ out.push(PacketS::Communicate {
+ message: None,
+ timeout: Some(0.),
+ player: me,
+ pin: Some(true),
+ });
+ out.push(PacketS::Effect {
+ name: "satisfied".to_string(),
+ player: me,
+ });
+ out.push(PacketS::Interact {
+ target: Some(ItemLocation::Tile(pos)),
+ player: me,
+ hand: Hand(0),
+ });
+ out.push(PacketS::ApplyScore(Score {
+ demands_completed: 1,
+ points,
+ ..Default::default()
+ }));
+ out.push(PacketS::Interact {
+ target: None,
+ player: me,
+ hand: Hand(0),
+ });
*self = CustomerState::Eating {
demand: *demand,
table: pos,
@@ -294,48 +310,15 @@ impl CustomerState {
chair: *chair,
origin: *origin,
};
- return BotInput {
- extra: vec![
- PacketS::Communicate {
- message: None,
- timeout: Some(0.),
- player: me,
- pin: Some(false),
- },
- PacketS::Communicate {
- message: None,
- timeout: Some(0.),
- player: me,
- pin: Some(true),
- },
- PacketS::Effect {
- name: "satisfied".to_string(),
- player: me,
- },
- PacketS::Interact {
- target: Some(ItemLocation::Tile(pos)),
- player: me,
- hand: Hand(0),
- },
- PacketS::ApplyScore(Score {
- demands_completed: 1,
- points,
- ..Default::default()
- }),
- PacketS::Interact {
- target: None,
- player: me,
- hand: Hand(0),
- },
- ],
- ..Default::default()
- };
+ return;
}
}
- BotInput {
- direction: dir_input(pos, *chair, *facing),
- ..Default::default()
- }
+ out.push(PacketS::Movement {
+ player: me,
+ dir: dir_input(pos, *chair, *facing),
+ boost: false,
+ pos: None,
+ });
}
CustomerState::Eating {
@@ -354,19 +337,19 @@ impl CustomerState {
origin: *origin,
cooldown: 0.5,
};
- return BotInput {
- extra: vec![PacketS::ReplaceHand {
- player: me,
- item: demand.output,
- hand: Hand(0),
- }],
- ..Default::default()
- };
- }
- BotInput {
- direction: dir_input(pos, *chair, (*table - *chair).as_vec2()),
- ..Default::default()
+ out.push(PacketS::ReplaceHand {
+ player: me,
+ item: demand.output,
+ hand: Hand(0),
+ });
+ return;
}
+ out.push(PacketS::Movement {
+ player: me,
+ dir: dir_input(pos, *chair, (*table - *chair).as_vec2()),
+ boost: false,
+ pos: None,
+ });
}
CustomerState::Finishing {
table,
@@ -383,49 +366,42 @@ impl CustomerState {
if let Some(path) = find_path(game, pos.as_ivec2(), *origin) {
*self = CustomerState::Exiting { path };
}
- BotInput::default()
} else {
- let direction = (table.as_vec2() + 0.5) - pos;
if *cooldown < 0. {
*cooldown += 1.;
- BotInput {
- extra: vec![
- PacketS::Interact {
- player: me,
- target: Some(ItemLocation::Tile(*table)),
- hand: Hand(0),
- },
- PacketS::Interact {
- player: me,
- target: None,
- hand: Hand(0),
- },
- ],
- direction,
- ..Default::default()
- }
- } else {
- BotInput {
- direction,
- ..Default::default()
- }
+ out.push(PacketS::Interact {
+ player: me,
+ target: Some(ItemLocation::Tile(*table)),
+ hand: Hand(0),
+ });
+ out.push(PacketS::Interact {
+ player: me,
+ target: None,
+ hand: Hand(0),
+ });
}
+ out.push(PacketS::Movement {
+ player: me,
+ dir: (table.as_vec2() + 0.5) - pos,
+ boost: false,
+ pos: None,
+ });
}
}
CustomerState::Exiting { path } => {
if path.is_done() || path.is_stuck() {
debug!("{me:?} -> leave");
- BotInput {
- leave: true,
- ..Default::default()
- }
+ *self = CustomerState::Exited
} else {
- BotInput {
- direction: path.next_direction(pos, dt) * 0.6,
- ..Default::default()
- }
+ out.push(PacketS::Movement {
+ player: me,
+ dir: path.next_direction(pos, dt) * 0.6,
+ boost: false,
+ pos: None,
+ });
}
}
+ CustomerState::Exited => (),
}
}
}
diff --git a/server/bot/src/algos/dishwasher.rs b/server/bot/src/algos/dishwasher.rs
index 5dbf954f..563e8c54 100644
--- a/server/bot/src/algos/dishwasher.rs
+++ b/server/bot/src/algos/dishwasher.rs
@@ -16,76 +16,53 @@
*/
use super::simple::State;
-use crate::{BotAlgo, BotInput, algos::simple::Context, pathfinding::Path};
+use crate::{BotAlgo, PacketSink, algos::simple::Context, step::StepState};
use hurrycurry_game_core::Game;
-use hurrycurry_protocol::{ItemIndex, PlayerID, glam::IVec2};
+use hurrycurry_protocol::{ItemIndex, PlayerID};
-#[derive(Default)]
pub struct DishWasher {
- path: Option<(Path, IVec2, f32)>,
- cooldown: f32,
+ me: PlayerID,
+ step: StepState,
dirty_plate: Option<ItemIndex>,
}
type LogicRes<Out = ()> = Result<Out, ()>;
+impl DishWasher {
+ pub fn new(me: PlayerID) -> Self {
+ Self {
+ me,
+ step: StepState::new_idle(me),
+ dirty_plate: None,
+ }
+ }
+}
impl BotAlgo for DishWasher {
- fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput {
+ fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) {
if self.dirty_plate.is_none() {
self.dirty_plate = game.data.get_item_by_name("dirty-plate");
if self.dirty_plate.is_none() {
- return BotInput::default();
+ return;
}
}
-
- let Some(player) = game.players.get(&me) else {
- return BotInput::default();
- };
- let pos = player.movement.position;
-
- if self.cooldown > 0. {
- self.cooldown -= dt;
- return BotInput::default();
+ if self.step.is_busy() {
+ return self.step.tick(&mut out, game, dt);
}
- if let Some((path, target, down)) = &mut self.path {
- let direction = path.next_direction(pos, dt);
- let arrived = path.is_done();
- let target = *target;
- if arrived {
- *down -= dt;
- if *down < 0. {
- self.path = None;
- self.cooldown = 0.2;
- }
- }
- return BotInput {
- direction,
- boost: false,
- interact: if arrived { Some(target) } else { None },
- ..Default::default()
- };
- }
Context {
game,
- own_position: pos.as_ivec2(),
- me,
+ me: self.me,
state: self,
recursion_abort: 0,
}
.update()
.ok();
-
- BotInput::default()
}
}
impl State for DishWasher {
- fn cooldown(&mut self, dur: f32) {
- self.cooldown += dur
- }
- fn queue_segment(&mut self, path: Path, tile: IVec2, duration: f32) {
- self.path = Some((path, tile, duration));
+ fn queue_step(&mut self, step: StepState) {
+ self.step = step;
}
fn get_empty_tile_priority(&self) -> &'static [&'static str] {
&["counter-window", "counter"]
diff --git a/server/bot/src/algos/frank.rs b/server/bot/src/algos/frank.rs
index 489417a6..eeee3b61 100644
--- a/server/bot/src/algos/frank.rs
+++ b/server/bot/src/algos/frank.rs
@@ -1,5 +1,3 @@
-use std::random::random;
-
/*
Hurry Curry! - a game about cooking
Copyright (C) 2025 Hurry Curry! Contributors
@@ -18,14 +16,16 @@ use std::random::random;
*/
use crate::{
- BotAlgo, BotInput,
+ BotAlgo, PacketSink,
pathfinding::{Path, find_path_to_neighbour},
};
use hurrycurry_game_core::Game;
use hurrycurry_protocol::{Message, PacketS, PlayerClass, PlayerID, glam::Vec2};
+use std::random::random;
-#[derive(Default)]
pub struct Frank {
+ me: PlayerID,
+
sleep: f32,
idle: f32,
idle_dir: f32,
@@ -34,23 +34,38 @@ pub struct Frank {
path: Option<Path>,
}
+impl Frank {
+ pub fn new(me: PlayerID) -> Self {
+ Self {
+ me,
+ idle: 0.,
+ idle_dir: 0.,
+ path: None,
+ sleep: 0.,
+ target: None,
+ }
+ }
+}
+
impl BotAlgo for Frank {
- fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput {
- let Some(player) = game.players.get(&me) else {
- return BotInput::default();
+ fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) {
+ let Some(player) = game.players.get(&self.me) else {
+ return;
};
if self.sleep > 0. {
self.sleep -= dt;
- return BotInput::default();
+ return;
}
if self.idle > 0. {
self.idle -= dt;
self.idle_dir += self.idle.sin() * dt * (self.idle / 10.).fract() * 10.;
- return BotInput {
- direction: Vec2::from_angle(self.idle_dir),
- ..Default::default()
- };
+ out.push(PacketS::Movement {
+ player: self.me,
+ dir: Vec2::from_angle(self.idle_dir),
+ boost: false,
+ pos: None,
+ });
}
let pos = player.movement.position;
@@ -68,26 +83,25 @@ impl BotAlgo for Frank {
if pos.distance(tpos) < 2. {
self.sleep = 8.;
self.idle = 15.;
- return BotInput {
- extra: vec![PacketS::Communicate {
- player: me,
- message: Some(Message::Translation {
- id: format!("s.bot.frank.line.{}", random::<u32>(..) % 8),
- params: vec![Message::Text(player.name.clone())],
- }),
- timeout: Some(3.),
- pin: Some(false),
- }],
- ..Default::default()
- };
+ out.push(PacketS::Communicate {
+ player: self.me,
+ message: Some(Message::Translation {
+ id: format!("s.bot.frank.line.{}", random::<u32>(..) % 8),
+ params: vec![Message::Text(player.name.clone())],
+ }),
+ timeout: Some(3.),
+ pin: Some(false),
+ });
+ return;
}
} else {
- return BotInput {
- direction,
+ out.push(PacketS::Movement {
+ player: self.me,
+ dir: direction,
boost: false,
- interact: None,
- ..Default::default()
- };
+ pos: None,
+ });
+ return;
}
} else if let Some(path) =
find_path_to_neighbour(game, pos.as_ivec2(), tpos.as_ivec2())
@@ -97,10 +111,9 @@ impl BotAlgo for Frank {
} else {
self.target = None;
}
- } else if let Some(target) = find_chef(game, me) {
+ } else if let Some(target) = find_chef(game, self.me) {
self.target = Some(target);
}
- BotInput::default()
}
}
diff --git a/server/bot/src/algos/mod.rs b/server/bot/src/algos/mod.rs
index ad6b1586..178ecec2 100644
--- a/server/bot/src/algos/mod.rs
+++ b/server/bot/src/algos/mod.rs
@@ -19,22 +19,22 @@ mod customer;
mod dishwasher;
mod frank;
mod simple;
-mod test;
mod waiter;
pub use customer::{Customer, CustomerConfig};
pub use dishwasher::DishWasher;
pub use frank::Frank;
+use hurrycurry_protocol::PlayerID;
pub use simple::Simple;
-pub use test::Test;
pub use waiter::Waiter;
+pub type BotAlgoConstructor = fn(PlayerID) -> crate::DynBotAlgo;
+
#[allow(clippy::type_complexity)]
-pub const ALGO_CONSTRUCTORS: &[(&str, fn() -> crate::DynBotAlgo)] = &[
- ("test", || Box::new(Test::default())),
- ("simple", || Box::new(Simple::default())),
- ("waiter", || Box::new(Waiter::default())),
- ("dishwasher", || Box::new(DishWasher::default())),
- ("customer", || Box::new(Customer::default())),
- ("frank", || Box::new(Frank::default())),
+pub const ALGO_CONSTRUCTORS: &[(&str, BotAlgoConstructor)] = &[
+ ("simple", |p| Box::new(Simple::new(p))),
+ ("waiter", |p| Box::new(Waiter::new(p))),
+ ("dishwasher", |p| Box::new(DishWasher::new(p))),
+ ("customer", |p| Box::new(Customer::new(p))),
+ ("frank", |p| Box::new(Frank::new(p))),
];
diff --git a/server/bot/src/algos/simple.rs b/server/bot/src/algos/simple.rs
index 9c17cd82..050c2081 100644
--- a/server/bot/src/algos/simple.rs
+++ b/server/bot/src/algos/simple.rs
@@ -15,89 +15,59 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-use crate::{
- BotAlgo, BotInput,
- pathfinding::{Path, find_path_to_neighbour},
-};
+use crate::{BotAlgo, PacketSink, step::StepState};
use hurrycurry_game_core::Game;
use hurrycurry_protocol::{
- ItemIndex, Message, PlayerID, Recipe, RecipeIndex, TileIndex, glam::IVec2,
+ Hand, ItemIndex, Message, PlayerID, Recipe, RecipeIndex, TileIndex, glam::IVec2,
};
use log::{debug, warn};
-#[derive(Default)]
pub struct Simple {
- path: Option<(Path, IVec2, f32)>,
- cooldown: f32,
+ step: StepState,
+ me: PlayerID,
}
pub struct Context<'a, State> {
pub game: &'a Game,
pub me: PlayerID,
- pub own_position: IVec2,
pub state: &'a mut State,
pub recursion_abort: usize,
}
type LogicRes<Out = ()> = Result<Out, ()>;
-impl BotAlgo for Simple {
- fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput {
- let Some(player) = game.players.get(&me) else {
- return BotInput::default();
- };
- let pos = player.movement.position;
-
- if self.cooldown > 0. {
- self.cooldown -= dt;
- return BotInput::default();
+impl Simple {
+ pub fn new(me: PlayerID) -> Self {
+ Self {
+ me,
+ step: StepState::new_idle(me),
}
-
- if let Some((path, target, down)) = &mut self.path {
- let direction = path.next_direction(pos, dt);
- let arrived = path.is_done();
- let target = *target;
- if arrived {
- *down -= dt;
- if *down < 0. {
- self.path = None;
- self.cooldown = 0.2;
- }
- }
- return BotInput {
- direction,
- boost: false,
- interact: if arrived { Some(target) } else { None },
- ..Default::default()
- };
+ }
+}
+impl BotAlgo for Simple {
+ fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) {
+ if self.step.is_busy() {
+ return self.step.tick(&mut out, game, dt);
}
Context {
game,
- own_position: pos.as_ivec2(),
- me,
+ me: self.me,
state: self,
recursion_abort: 0,
}
.update()
.ok();
- debug!("target={:?}", self.path.as_ref().map(|a| a.1));
-
- BotInput::default()
}
}
pub trait State {
- fn cooldown(&mut self, duration: f32);
- fn queue_segment(&mut self, path: Path, tile: IVec2, duration: f32);
+ fn queue_step(&mut self, step: StepState);
fn get_empty_tile_priority(&self) -> &'static [&'static str];
}
impl State for Simple {
- fn cooldown(&mut self, duration: f32) {
- self.cooldown = duration;
- }
- fn queue_segment(&mut self, path: Path, tile: IVec2, duration: f32) {
- self.path = Some((path, tile, duration));
+ fn queue_step(&mut self, step: StepState) {
+ self.step = step;
}
fn get_empty_tile_priority(&self) -> &'static [&'static str] {
&["counter", "counter-window"]
@@ -243,8 +213,8 @@ impl<S: State> Context<'_, S> {
}
}
pub fn interact_with(&mut self, tile: IVec2, duration: f32) -> LogicRes {
- if let Some(path) = find_path_to_neighbour(self.game, self.own_position, tile) {
- self.state.queue_segment(path, tile, duration);
+ if let Some(step) = StepState::new_segment(self.game, self.me, Hand(0), tile, duration) {
+ self.state.queue_step(step);
Err(())
} else {
Ok(())
@@ -333,7 +303,7 @@ impl Context<'_, Simple> {
if let Some(item) = &self.game.tiles.get(&pos).unwrap().item {
if item.kind == *input {
debug!("waiting for passive to finish at {pos}");
- self.state.cooldown(0.5);
+ self.state.queue_step(StepState::new_wait(self.me, 0.5));
return Err(()); // waiting for it to finish
} else {
self.assert_tile_is_clear(pos)?;
@@ -348,7 +318,7 @@ impl Context<'_, Simple> {
} => {
self.aquire_placed_item(*input)?;
debug!("waiting for passive to finish");
- self.state.cooldown(0.5);
+ self.state.queue_step(StepState::new_wait(self.me, 0.5));
return Err(());
}
_ => warn!("recipe too hard {r:?}"),
diff --git a/server/bot/src/algos/test.rs b/server/bot/src/algos/test.rs
deleted file mode 100644
index 387dd285..00000000
--- a/server/bot/src/algos/test.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- 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
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use crate::{
- BotAlgo, BotInput,
- pathfinding::{Path, find_path_to_neighbour},
-};
-use hurrycurry_game_core::Game;
-use hurrycurry_protocol::{ItemIndex, Message, PlayerID, glam::IVec2};
-use log::info;
-
-#[derive(Default)]
-pub struct Test {
- path: Option<Path>,
-}
-
-impl BotAlgo for Test {
- fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput {
- let Some(player) = game.players.get(&me) else {
- return BotInput::default();
- };
- let pos = player.movement.position;
-
- if let Some(path) = &mut self.path {
- let direction = path.next_direction(pos, dt);
- return BotInput {
- direction,
- boost: false,
- interact: None,
- ..Default::default()
- };
- } else if let Some((item, near)) = find_demand(game) {
- info!("demand {item:?} near {near}");
- if let Some(path) = find_path_to_neighbour(game, pos.as_ivec2(), near) {
- self.path = Some(path);
- }
- }
- BotInput::default()
- }
-}
-
-fn find_demand(game: &Game) -> Option<(ItemIndex, IVec2)> {
- game.players
- .iter()
- .find_map(|(_, pl)| match &pl.communicate_persist {
- Some((Message::Item(item), _)) => {
- let pos = pl.movement.position.as_ivec2();
- let t = pos;
- Some((*item, t))
- }
- _ => None,
- })
-}
diff --git a/server/bot/src/algos/waiter.rs b/server/bot/src/algos/waiter.rs
index 9273eb42..b1400ed2 100644
--- a/server/bot/src/algos/waiter.rs
+++ b/server/bot/src/algos/waiter.rs
@@ -16,69 +16,46 @@
*/
use super::simple::State;
-use crate::{BotAlgo, BotInput, algos::simple::Context, pathfinding::Path};
+use crate::{BotAlgo, PacketSink, algos::simple::Context, step::StepState};
use hurrycurry_game_core::Game;
-use hurrycurry_protocol::{ItemIndex, PlayerID, glam::IVec2};
+use hurrycurry_protocol::{ItemIndex, PlayerID};
use log::debug;
-#[derive(Default)]
pub struct Waiter {
- path: Option<(Path, IVec2, f32)>,
- cooldown: f32,
+ step: StepState,
+ me: PlayerID,
}
type LogicRes<Out = ()> = Result<Out, ()>;
+impl Waiter {
+ pub fn new(me: PlayerID) -> Self {
+ Self {
+ step: StepState::new_idle(me),
+ me,
+ }
+ }
+}
impl BotAlgo for Waiter {
- fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput {
- let Some(player) = game.players.get(&me) else {
- return BotInput::default();
- };
- let pos = player.movement.position;
-
- if self.cooldown > 0. {
- self.cooldown -= dt;
- return BotInput::default();
+ fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) {
+ if self.step.is_busy() {
+ return self.step.tick(&mut out, game, dt);
}
- if let Some((path, target, down)) = &mut self.path {
- let direction = path.next_direction(pos, dt);
- let arrived = path.is_done();
- let target = *target;
- if arrived {
- *down -= dt;
- if *down < 0. {
- self.path = None;
- self.cooldown = 0.2;
- }
- }
- return BotInput {
- direction,
- boost: false,
- interact: if arrived { Some(target) } else { None },
- ..Default::default()
- };
- }
Context {
game,
- own_position: pos.as_ivec2(),
- me,
+ me: self.me,
state: self,
recursion_abort: 0,
}
.update()
.ok();
-
- BotInput::default()
}
}
impl State for Waiter {
- fn cooldown(&mut self, dur: f32) {
- self.cooldown += dur
- }
- fn queue_segment(&mut self, path: Path, tile: IVec2, duration: f32) {
- self.path = Some((path, tile, duration));
+ fn queue_step(&mut self, step: StepState) {
+ self.step = step;
}
fn get_empty_tile_priority(&self) -> &'static [&'static str] {
&["counter-window", "counter"]
diff --git a/server/bot/src/lib.rs b/server/bot/src/lib.rs
index a3889021..337d18e8 100644
--- a/server/bot/src/lib.rs
+++ b/server/bot/src/lib.rs
@@ -18,31 +18,37 @@
#![feature(random)]
pub mod algos;
pub mod pathfinding;
+pub mod step;
use hurrycurry_game_core::Game;
-use hurrycurry_protocol::{
- PacketS, PlayerID,
- glam::{IVec2, Vec2},
-};
-use std::random::random;
+use hurrycurry_protocol::PacketS;
+use std::{collections::VecDeque, random::random};
-#[derive(Default, Clone)]
-pub struct BotInput {
- pub direction: Vec2,
- pub boost: bool,
- pub interact: Option<IVec2>,
- pub leave: bool,
- pub extra: Vec<PacketS>,
+pub struct PacketSink<'a> {
+ buf: &'a mut VecDeque<PacketS>,
+}
+impl<'a> PacketSink<'a> {
+ pub fn new(buf: &'a mut VecDeque<PacketS>) -> Self {
+ Self { buf }
+ }
+ pub fn push(&mut self, p: PacketS) {
+ self.buf.push_back(p);
+ }
}
pub type DynBotAlgo = Box<dyn BotAlgo + Send + Sync + 'static>;
pub trait BotAlgo {
- fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput;
+ fn tick(&mut self, out: PacketSink, game: &Game, dt: f32);
+ fn is_finished(&self) -> bool {
+ false
+ }
}
-
impl<T: BotAlgo + ?Sized> BotAlgo for Box<T> {
- fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput {
- (**self).tick(me, game, dt)
+ fn tick(&mut self, out: PacketSink, game: &Game, dt: f32) {
+ (**self).tick(out, game, dt)
+ }
+ fn is_finished(&self) -> bool {
+ (**self).is_finished()
}
}
diff --git a/server/bot/src/main.rs b/server/bot/src/main.rs
index 2fbf274f..fff25d63 100644
--- a/server/bot/src/main.rs
+++ b/server/bot/src/main.rs
@@ -17,9 +17,9 @@
*/
use anyhow::Result;
use clap::Parser;
-use hurrycurry_bot::{BotAlgo, BotInput, algos::ALGO_CONSTRUCTORS};
+use hurrycurry_bot::{BotAlgo, PacketSink, algos::ALGO_CONSTRUCTORS};
use hurrycurry_game_core::{Game, network::sync::Network};
-use hurrycurry_protocol::{Character, Hand, ItemLocation, PacketC, PacketS, PlayerClass, PlayerID};
+use hurrycurry_protocol::{Character, PacketC, PacketS, PlayerClass, PlayerID};
use log::warn;
use std::{thread::sleep, time::Duration};
@@ -87,7 +87,7 @@ fn main() -> Result<()> {
state: ALGO_CONSTRUCTORS
.iter()
.find(|(n, _)| n == &args.algo)
- .map(|(_, c)| c())
+ .map(|(_, c)| c(*id))
.unwrap_or_else(|| panic!("unknown algo {:?}", args.algo)),
}),
PacketC::ServerMessage {
@@ -102,34 +102,13 @@ fn main() -> Result<()> {
}
bots.retain_mut(|b| {
- let BotInput {
- direction,
- boost,
- interact,
- leave,
- extra,
- } = b.state.tick(b.id, &game, dt);
+ b.state
+ .tick(PacketSink::new(&mut network.queue_out), &game, dt);
- if leave {
+ if b.state.is_finished() {
network.queue_out.push_back(PacketS::Leave { player: b.id });
return false;
}
-
- if interact.is_some() != b.interacting {
- b.interacting = interact.is_some();
- network.queue_out.push_back(PacketS::Interact {
- player: b.id,
- target: interact.map(ItemLocation::Tile),
- hand: Hand(0),
- })
- }
- network.queue_out.push_back(PacketS::Movement {
- player: b.id,
- dir: direction,
- boost,
- pos: None,
- });
- network.queue_out.extend(extra);
true
});
diff --git a/server/bot/src/pathfinding.rs b/server/bot/src/pathfinding.rs
index e17ca80c..ec098495 100644
--- a/server/bot/src/pathfinding.rs
+++ b/server/bot/src/pathfinding.rs
@@ -31,6 +31,10 @@ pub struct Path {
}
impl Path {
+ pub const EMPTY: Path = Path {
+ seg_time: 0.,
+ segments: Vec::new(),
+ };
pub fn next_direction(&mut self, position: Vec2, dt: f32) -> Vec2 {
if let Some(next) = self.segments.last().copied() {
trace!("next {next}");
diff --git a/server/bot/src/step.rs b/server/bot/src/step.rs
new file mode 100644
index 00000000..001935ac
--- /dev/null
+++ b/server/bot/src/step.rs
@@ -0,0 +1,121 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright (C) 2025 Hurry Curry! Contributors
+
+ 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
+ the Free Software Foundation, version 3 of the License only.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+*/
+
+use crate::{
+ PacketSink,
+ pathfinding::{Path, find_path_to_neighbour},
+};
+use hurrycurry_game_core::Game;
+use hurrycurry_protocol::{Hand, ItemIndex, ItemLocation, PacketS, PlayerID, glam::IVec2};
+
+pub struct StepState {
+ path: Path,
+ interact_position: IVec2,
+ destination_item: Option<ItemIndex>,
+ hand: Hand,
+ wait_timer: f32,
+ interact_start_pending: bool,
+ interact_stop_pending: bool,
+ me: PlayerID,
+}
+
+impl StepState {
+ pub fn new_idle(me: PlayerID) -> Self {
+ Self::new_wait(me, 0.)
+ }
+ pub fn new_wait(me: PlayerID, timer: f32) -> Self {
+ Self {
+ destination_item: None,
+ interact_position: IVec2::ZERO,
+ hand: Hand(0),
+ interact_start_pending: false,
+ interact_stop_pending: false,
+ me,
+ path: Path::EMPTY,
+ wait_timer: timer,
+ }
+ }
+ pub fn new_segment(
+ game: &Game,
+ me: PlayerID,
+ hand: Hand,
+ pos: IVec2,
+ interact: f32,
+ ) -> Option<Self> {
+ let own_pos = game.players.get(&me)?.movement.position.as_ivec2();
+ let path = find_path_to_neighbour(game, own_pos, pos)?;
+ Some(Self {
+ me,
+ path,
+ hand,
+ wait_timer: interact,
+ interact_stop_pending: true,
+ interact_start_pending: true,
+ interact_position: pos,
+ destination_item: game
+ .tiles
+ .get(&pos)
+ .and_then(|t| t.item.as_ref().map(|i| i.kind)),
+ })
+ }
+
+ pub fn is_busy(&self) -> bool {
+ self.wait_timer >= 0. || self.interact_stop_pending
+ }
+
+ pub fn tick(&mut self, out: &mut PacketSink, game: &Game, dt: f32) {
+ if self.path.is_done() {
+ if self.interact_start_pending {
+ self.interact_start_pending = false;
+ out.push(PacketS::Interact {
+ player: self.me,
+ hand: self.hand,
+ target: Some(ItemLocation::Tile(self.interact_position)),
+ });
+ } else {
+ if self.wait_timer < 0. {
+ if self.interact_stop_pending {
+ self.interact_stop_pending = false;
+ out.push(PacketS::Interact {
+ player: self.me,
+ hand: self.hand,
+ target: None,
+ });
+ }
+ } else {
+ self.wait_timer -= dt;
+ }
+ }
+ }
+
+ let position = game
+ .players
+ .get(&self.me)
+ .as_ref()
+ .map(|p| p.movement.position)
+ .unwrap_or_default();
+
+ let dir = self.path.next_direction(position, dt);
+ out.push(PacketS::Movement {
+ player: self.me,
+ dir,
+ boost: false,
+ pos: None,
+ });
+ }
+}
diff --git a/server/data/src/lib.rs b/server/data/src/lib.rs
index 5d30afb7..09a1bd43 100644
--- a/server/data/src/lib.rs
+++ b/server/data/src/lib.rs
@@ -26,7 +26,7 @@ use anyhow::{Result, anyhow, bail};
use clap::Parser;
use filter_demands::filter_demands_and_recipes;
use hurrycurry_protocol::{
- Demand, Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex,
+ Demand, Gamedata, GamedataFlags, ItemIndex, MapMetadata, Recipe, TileIndex,
book::Book,
glam::{IVec2, Vec2},
};
@@ -78,7 +78,7 @@ pub struct MapDecl {
#[serde(default)] entities: Vec<EntityDecl>,
#[serde(default)] score_baseline: i64,
#[serde(default)] default_timer: Option<u64>,
- #[serde(default)] flags: ServerdataFlags,
+ #[serde(default)] flags: GamedataFlags,
}
#[derive(Parser)]
@@ -119,17 +119,10 @@ pub struct Serverdata {
pub score_baseline: i64,
pub default_timer: Option<Duration>,
pub book: Book,
- pub flags: ServerdataFlags,
pub entity_decls: Vec<EntityDecl>,
pub recipe_groups: BTreeMap<String, BTreeSet<ItemIndex>>,
}
-#[rustfmt::skip]
-#[derive(Debug, Clone, Default, Deserialize)]
-pub struct ServerdataFlags {
- #[serde(default)] pub disable_unknown_orders: bool,
-}
-
fn build_data(
maps: &HashMap<String, MapMetadata>,
map_name: String,
@@ -272,6 +265,7 @@ fn build_data(
tile_walkable,
tile_placeable_items,
tile_interactable_empty,
+ flags: map_in.flags,
recipes,
item_names,
demands,
@@ -287,7 +281,6 @@ fn build_data(
let mut serverdata = Serverdata {
initial_map,
chef_spawn,
- flags: map_in.flags,
customer_spawn,
default_timer,
book: Book::default(),
diff --git a/server/protocol/src/helpers.rs b/server/protocol/src/helpers.rs
index 31e68d02..90dfd6ff 100644
--- a/server/protocol/src/helpers.rs
+++ b/server/protocol/src/helpers.rs
@@ -15,8 +15,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-use crate::{Gamedata, Hand, ItemIndex, ItemLocation, PlayerID, Recipe, RecipeIndex, TileIndex};
-use std::fmt::Display;
+use crate::{Gamedata, ItemIndex, Recipe, RecipeIndex, TileIndex};
impl Gamedata {
pub fn tile_name(&self, index: TileIndex) -> &str {
@@ -102,37 +101,89 @@ impl Recipe {
}
}
-impl Display for ItemLocation {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- ItemLocation::Tile(pos) => write!(f, "tile({}, {})", pos.x, pos.y),
- ItemLocation::Player(player, hand) => write!(f, "{player}-{hand}"),
+mod display {
+ use crate::*;
+ use std::fmt::Display;
+
+ impl Display for ItemLocation {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ItemLocation::Tile(pos) => write!(f, "tile({}, {})", pos.x, pos.y),
+ ItemLocation::Player(player, hand) => write!(f, "{player}-{hand}"),
+ }
+ }
+ }
+ impl Display for Hand {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "hand#{}", self.0)
+ }
+ }
+ impl Display for PlayerID {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "player#{}", self.0)
+ }
+ }
+ impl Display for TileIndex {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "tile#{}", self.0)
+ }
+ }
+ impl Display for ItemIndex {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "item#{}", self.0)
+ }
+ }
+ impl Display for RecipeIndex {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "recipe#{}", self.0)
}
}
}
-impl Display for Hand {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "hand#{}", self.0)
+mod enum_is {
+ use crate::*;
+
+ impl PlayerClass {
+ pub fn is_cheflike(&self) -> bool {
+ matches!(self, Self::Bot | Self::Chef)
+ }
}
-}
-impl Display for PlayerID {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "player#{}", self.0)
+
+ impl ItemLocation {
+ pub fn is_tile(&self) -> bool {
+ matches!(self, Self::Tile(..))
+ }
+ pub fn is_player(&self) -> bool {
+ matches!(self, Self::Player(..))
+ }
}
}
-impl Display for TileIndex {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "tile#{}", self.0)
+
+pub mod deser {
+ use crate::*;
+ use serde::Deserializer;
+ use std::collections::BTreeMap;
+
+ pub(crate) fn deser_i64<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
+ let x = f64::deserialize(deserializer)?;
+ Ok(x.trunc() as i64)
}
-}
-impl Display for ItemIndex {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "item#{}", self.0)
+ pub(crate) fn deser_i32<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i32, D::Error> {
+ let x = f64::deserialize(deserializer)?;
+ Ok(x.trunc() as i32)
}
-}
-impl Display for RecipeIndex {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "recipe#{}", self.0)
+ pub(crate) fn deser_usize<'de, D: Deserializer<'de>>(
+ deserializer: D,
+ ) -> Result<usize, D::Error> {
+ let x = f64::deserialize(deserializer)?;
+ Ok(x.trunc() as usize)
+ }
+ pub(crate) fn deser_tile_index_map<'de, D: Deserializer<'de>>(
+ deserializer: D,
+ ) -> Result<BTreeMap<TileIndex, HashSet<ItemIndex>>, D::Error> {
+ let x = BTreeMap::<String, HashSet<ItemIndex>>::deserialize(deserializer)?;
+ Ok(x.into_iter()
+ .map(|(k, v)| (TileIndex(k.parse().ok().unwrap_or_default()), v))
+ .collect())
}
}
diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs
index 2be37730..a89d9c30 100644
--- a/server/protocol/src/lib.rs
+++ b/server/protocol/src/lib.rs
@@ -15,8 +15,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+use crate::book::Book;
use glam::{IVec2, Vec2};
-use serde::{Deserialize, Deserializer, Serialize};
+use helpers::deser::*;
+use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, BTreeSet, HashSet},
sync::LazyLock,
@@ -24,8 +26,6 @@ use std::{
pub use glam;
-use crate::book::Book;
-
pub mod book;
pub mod helpers;
pub mod movement;
@@ -94,8 +94,18 @@ pub struct Gamedata {
pub recipes: Vec<Recipe>,
pub demands: Vec<Demand>,
pub hand_count: usize,
+ pub flags: GamedataFlags,
+}
+
+#[rustfmt::skip]
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct GamedataFlags {
+ #[serde(default)] pub disable_unknown_orders: bool,
}
+fn chef_class() -> PlayerClass {
+ PlayerClass::Chef
+}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum PacketS {
@@ -139,22 +149,28 @@ pub enum PacketS {
dt: f64,
},
- #[serde(skip)]
/// For internal use only (customers)
+ #[serde(skip)]
ReplaceHand {
player: PlayerID,
hand: Hand,
item: Option<ItemIndex>,
},
- #[serde(skip)]
+
/// For internal use only (customers)
- ApplyScore(Score),
#[serde(skip)]
+ ApplyScore(Score),
+
/// For internal use only (customers)
+ #[serde(skip)]
Effect {
player: PlayerID,
name: String,
},
+
+ /// Used internally and only used when built with debug_events
+ #[serde(skip)]
+ Debug(DebugEvent),
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
@@ -167,10 +183,6 @@ pub struct Character {
pub headwear: i32,
}
-fn chef_class() -> PlayerClass {
- PlayerClass::Chef
-}
-
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PlayerClass {
@@ -179,11 +191,6 @@ pub enum PlayerClass {
Customer,
Tram,
}
-impl PlayerClass {
- pub fn is_cheflike(&self) -> bool {
- matches!(self, Self::Bot | Self::Chef)
- }
-}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
@@ -294,6 +301,9 @@ pub enum PacketC {
/// For use in replay sessions only
ReplayStart,
ReplayStop,
+
+ /// Only used when built with debug_events
+ Debug(DebugEvent),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -369,32 +379,14 @@ pub enum ItemLocation {
Player(PlayerID, Hand),
}
-impl ItemLocation {
- pub fn is_tile(&self) -> bool {
- matches!(self, Self::Tile(..))
- }
- pub fn is_player(&self) -> bool {
- matches!(self, Self::Player(..))
- }
-}
-
-fn deser_i64<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
- let x = f64::deserialize(deserializer)?;
- Ok(x.trunc() as i64)
-}
-fn deser_i32<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i32, D::Error> {
- let x = f64::deserialize(deserializer)?;
- Ok(x.trunc() as i32)
-}
-fn deser_usize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<usize, D::Error> {
- let x = f64::deserialize(deserializer)?;
- Ok(x.trunc() as usize)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DebugEvent {
+ key: String,
+ display: Vec<DebugEventDisplay>,
+ timeout: Option<f32>,
}
-fn deser_tile_index_map<'de, D: Deserializer<'de>>(
- deserializer: D,
-) -> Result<BTreeMap<TileIndex, HashSet<ItemIndex>>, D::Error> {
- let x = BTreeMap::<String, HashSet<ItemIndex>>::deserialize(deserializer)?;
- Ok(x.into_iter()
- .map(|(k, v)| (TileIndex(k.parse().ok().unwrap_or_default()), v))
- .collect())
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum DebugEventDisplay {
+ Path(Vec<Vec2>),
+ Label(Vec2, String),
}
diff --git a/server/src/commands.rs b/server/src/commands.rs
index 0a7b7643..f8016c9f 100644
--- a/server/src/commands.rs
+++ b/server/src/commands.rs
@@ -283,7 +283,6 @@ impl Server {
.iter()
.find(|(name, _)| *name == algo.as_str())
.ok_or(tre!("s.error.algo_not_found", s = algo))?;
- let algo = cons();
self.entities.push(Box::new(BotDriver::new(
format!("{}-bot", name.unwrap_or((*aname).to_owned())),
Character {
@@ -292,7 +291,7 @@ impl Server {
headwear: 0,
},
PlayerClass::Bot,
- algo,
+ cons,
)));
}
Command::Scoreboard { map, text } => {
diff --git a/server/src/entity/bot.rs b/server/src/entity/bot.rs
index 51a09b62..36f701ec 100644
--- a/server/src/entity/bot.rs
+++ b/server/src/entity/bot.rs
@@ -17,77 +17,85 @@
*/
use super::{Entity, EntityContext};
use anyhow::Result;
-use hurrycurry_bot::{BotAlgo, DynBotAlgo};
+use hurrycurry_bot::{BotAlgo, DynBotAlgo, PacketSink};
use hurrycurry_locale::TrError;
-use hurrycurry_protocol::{Character, Hand, ItemLocation, PacketS, PlayerClass, PlayerID};
+use hurrycurry_protocol::{Character, PacketS, PlayerClass, PlayerID};
use log::debug;
use std::any::Any;
pub type DynBotDriver = BotDriver<DynBotAlgo>;
-pub struct BotDriver<T> {
- algo: T,
- join_data: Option<(String, Character, PlayerClass)>,
- id: PlayerID,
- interacting: bool,
- left: bool,
+pub enum BotDriver<T> {
+ Joining {
+ name: String,
+ character: Character,
+ class: PlayerClass,
+ constructor: Option<Box<dyn FnOnce(PlayerID) -> T + Send + Sync>>,
+ },
+ Running {
+ state: T,
+ id: PlayerID,
+ },
+ Left,
}
impl<T: BotAlgo> BotDriver<T> {
- pub fn new(name: String, character: Character, class: PlayerClass, algo: T) -> Self {
- Self {
- algo,
- id: PlayerID(0),
- interacting: false,
- join_data: Some((name, character, class)),
- left: false,
+ pub fn new(
+ name: String,
+ character: Character,
+ class: PlayerClass,
+ constructor: impl FnOnce(PlayerID) -> T + 'static + Send + Sync,
+ ) -> Self {
+ Self::Joining {
+ character,
+ class,
+ constructor: Some(Box::new(constructor)),
+ name,
}
}
}
impl<T: BotAlgo + Any> Entity for BotDriver<T> {
fn finished(&self) -> bool {
- self.left
+ matches!(self, Self::Left)
}
fn tick(&mut self, c: EntityContext<'_>) -> Result<(), TrError> {
- if let Some((name, character, class)) = self.join_data.take() {
- self.id = c.game.get_unused_player_id(); // TODO clashes when multiple bots join in the same tick
- debug!("join {}", self.id);
- c.packet_in.push_back(PacketS::Join {
+ match self {
+ BotDriver::Joining {
name,
character,
- id: Some(self.id),
class,
- position: None,
- })
+ constructor,
+ } => {
+ let id = c.game.get_unused_player_id(); // TODO clashes when multiple bots join in the same tick
+ debug!("enter {id}");
+ c.packet_in.push_back(PacketS::Join {
+ name: name.to_string(),
+ character: *character,
+ id: Some(id),
+ class: *class,
+ position: None,
+ });
+ *self = BotDriver::Running {
+ state: constructor.take().unwrap()(id),
+ id,
+ };
+ Ok(())
+ }
+ BotDriver::Running { state, id } => {
+ state.tick(PacketSink::new(c.packet_in), c.game, c.dt);
+ if state.is_finished() {
+ debug!("leave {id}");
+ c.packet_in.push_back(PacketS::Leave { player: *id });
+ *self = BotDriver::Left
+ }
+ Ok(())
+ }
+ BotDriver::Left => Ok(()),
}
-
- let input = self.algo.tick(self.id, c.game, c.dt);
- if input.leave {
- debug!("leave {}", self.id);
- c.packet_in.push_back(PacketS::Leave { player: self.id });
- self.left = true;
- return Ok(());
- }
- if input.interact.is_some() != self.interacting {
- self.interacting = input.interact.is_some();
- c.packet_in.push_back(PacketS::Interact {
- player: self.id,
- target: input.interact.map(ItemLocation::Tile),
- hand: Hand(0),
- })
- }
- c.packet_in.push_back(PacketS::Movement {
- player: self.id,
- dir: input.direction,
- boost: input.boost,
- pos: None,
- });
- c.packet_in.extend(input.extra);
- Ok(())
}
fn destructor(&mut self, c: EntityContext<'_>) {
- if self.join_data.is_none() && !self.left {
- c.packet_in.push_back(PacketS::Leave { player: self.id })
+ if let Self::Running { id, .. } = self {
+ c.packet_in.push_back(PacketS::Leave { player: *id })
}
}
}
diff --git a/server/src/entity/customers.rs b/server/src/entity/customers.rs
index 36afe24a..e527580c 100644
--- a/server/src/entity/customers.rs
+++ b/server/src/entity/customers.rs
@@ -18,7 +18,7 @@
use super::{Entity, EntityContext, bot::BotDriver};
use crate::random_float;
use anyhow::Result;
-use hurrycurry_bot::algos::{Customer, CustomerConfig};
+use hurrycurry_bot::algos::Customer;
use hurrycurry_locale::TrError;
use hurrycurry_protocol::{Character, PlayerClass};
use std::random::random;
@@ -64,9 +64,7 @@ impl Entity for Customers {
headwear: 0,
},
PlayerClass::Customer,
- Customer::new(CustomerConfig {
- unknown_order: !c.serverdata.flags.disable_unknown_orders,
- }),
+ |id| Customer::new(id),
);
self.customers.push(bot)
}
diff --git a/server/src/server.rs b/server/src/server.rs
index 12b45aa1..ae4e82d8 100644
--- a/server/src/server.rs
+++ b/server/src/server.rs
@@ -615,6 +615,7 @@ impl Server {
}
PacketS::ReplayTick { .. } => return Err(tre!("s.error.packet_not_supported")),
PacketS::Idle { .. } | PacketS::Ready => (),
+ PacketS::Debug(d) => self.packet_out.push_back(PacketC::Debug(d)),
}
Ok(())
}
diff --git a/server/src/state.rs b/server/src/state.rs
index 1e01036c..001e2bf0 100644
--- a/server/src/state.rs
+++ b/server/src/state.rs
@@ -236,16 +236,17 @@ impl Server {
fn get_packet_player(packet: &PacketS) -> Option<PlayerID> {
match packet {
- PacketS::Join { .. } => None,
- PacketS::Idle { .. } => 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::Effect { player, .. } => Some(*player),
- PacketS::Ready => None,
- PacketS::ApplyScore(_) => None,
- PacketS::ReplayTick { .. } => None,
+ PacketS::Leave { player }
+ | PacketS::Movement { player, .. }
+ | PacketS::Interact { player, .. }
+ | PacketS::Communicate { player, .. }
+ | PacketS::ReplaceHand { player, .. }
+ | PacketS::Effect { player, .. } => Some(*player),
+ PacketS::Join { .. }
+ | PacketS::Idle { .. }
+ | PacketS::Ready
+ | PacketS::ApplyScore(_)
+ | PacketS::ReplayTick { .. }
+ | PacketS::Debug(_) => None,
}
}