diff options
Diffstat (limited to 'server')
| -rw-r--r-- | server/Cargo.toml | 2 | ||||
| -rw-r--r-- | server/bot/Cargo.toml | 3 | ||||
| -rw-r--r-- | server/bot/src/algos/customer.rs | 278 | ||||
| -rw-r--r-- | server/bot/src/algos/dishwasher.rs | 63 | ||||
| -rw-r--r-- | server/bot/src/algos/frank.rs | 75 | ||||
| -rw-r--r-- | server/bot/src/algos/mod.rs | 18 | ||||
| -rw-r--r-- | server/bot/src/algos/simple.rs | 76 | ||||
| -rw-r--r-- | server/bot/src/algos/test.rs | 67 | ||||
| -rw-r--r-- | server/bot/src/algos/waiter.rs | 59 | ||||
| -rw-r--r-- | server/bot/src/lib.rs | 38 | ||||
| -rw-r--r-- | server/bot/src/main.rs | 33 | ||||
| -rw-r--r-- | server/bot/src/pathfinding.rs | 4 | ||||
| -rw-r--r-- | server/bot/src/step.rs | 121 | ||||
| -rw-r--r-- | server/data/src/lib.rs | 13 | ||||
| -rw-r--r-- | server/protocol/src/helpers.rs | 101 | ||||
| -rw-r--r-- | server/protocol/src/lib.rs | 76 | ||||
| -rw-r--r-- | server/src/commands.rs | 3 | ||||
| -rw-r--r-- | server/src/entity/bot.rs | 106 | ||||
| -rw-r--r-- | server/src/entity/customers.rs | 6 | ||||
| -rw-r--r-- | server/src/server.rs | 1 | ||||
| -rw-r--r-- | server/src/state.rs | 23 |
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, } } |