summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--server/src/customer.rs243
-rw-r--r--server/src/customer/mod.rs201
-rw-r--r--server/src/customer/movement.rs56
-rw-r--r--server/src/customer/pathfinding.rs83
-rw-r--r--server/src/data.rs3
5 files changed, 343 insertions, 243 deletions
diff --git a/server/src/customer.rs b/server/src/customer.rs
deleted file mode 100644
index 87b67f0e..00000000
--- a/server/src/customer.rs
+++ /dev/null
@@ -1,243 +0,0 @@
-use crate::{
- data::Gamedata,
- game::Game,
- protocol::{Message, PacketC, PacketS, PlayerID},
-};
-use glam::{IVec2, Vec2};
-use log::{debug, error};
-use rand::thread_rng;
-use std::{
- cmp::Ordering,
- collections::{BinaryHeap, HashMap, HashSet},
- sync::Arc,
- time::Duration,
-};
-use tokio::{
- sync::{broadcast, RwLock},
- time::interval,
-};
-
-struct DemandState {
- data: Gamedata,
- walkable: HashSet<IVec2>,
- chairs: HashMap<IVec2, bool>,
- customers: Vec<Customer>,
-}
-
-enum CustomerState {
- WalkingToChair { path: Vec<Vec2>, chair: IVec2 },
- Waiting { chair: IVec2 },
-}
-
-struct Customer {
- id: PlayerID,
- position: Vec2,
- facing: Vec2,
- vel: Vec2,
- state: CustomerState,
-}
-
-pub async fn customer(game: Arc<RwLock<Game>>, mut grx: broadcast::Receiver<PacketC>) {
- let mut state = DemandState {
- walkable: Default::default(),
- chairs: Default::default(),
- customers: Default::default(),
- data: Gamedata::default(),
- };
- let initial = game.write().await.prime_client(-1);
- for p in initial {
- match p {
- PacketC::Init { data, .. } => {
- state.data = data;
- }
- PacketC::UpdateMap { pos, tile, .. } => {
- let tilename = &state.data.tile_names[tile];
- if tilename == "floor" || tilename == "door" || tilename == "chair" {
- state.walkable.insert(pos);
- }
- if tilename == "chair" {
- state.chairs.insert(pos, true);
- }
- }
- _ => (),
- }
- }
-
- let mut interval = interval(Duration::from_millis(40));
- let mut packets_out = Vec::new();
- loop {
- tokio::select! {
- packet = grx.recv() => {
- match packet.unwrap() {
- // TODO handle map update
- _ => ()
- }
- }
- _ = interval.tick() => {
- state.tick(&mut packets_out, 0.04);
- for (player,packet) in packets_out.drain(..) {
- if let Err(e) = game.write().await.packet_in(player, packet) {
- error!("customer misbehaved: {e}")
- }
- }
- }
- }
- }
-}
-
-impl DemandState {
- pub fn tick(&mut self, packets_out: &mut Vec<(PlayerID, PacketS)>, dt: f32) {
- if self.customers.is_empty() {
- packets_out.push((
- -1,
- PacketS::Join {
- name: "George".to_string(),
- character: 0,
- },
- ));
- let chair = select_chair(&mut self.chairs);
- let path = find_path(&self.walkable, self.data.customer_spawn.as_ivec2(), chair)
- .expect("no path");
- self.customers.push(Customer {
- id: -1,
- position: self.data.customer_spawn,
- facing: Vec2::X,
- vel: Vec2::ZERO,
- state: CustomerState::WalkingToChair { path, chair },
- });
- }
-
- for p in &mut self.customers {
- match &mut p.state {
- CustomerState::WalkingToChair { path, chair } => {
- if let Some(next) = path.last().copied() {
- debug!("next {next}");
- if next.distance(p.position) < if path.len() == 1 { 0.1 } else { 0.6 } {
- path.pop();
- }
- packets_out
- .push((p.id, move_player(p, &self.walkable, next - p.position, dt)));
- } else {
- packets_out.push((
- p.id,
- PacketS::Communicate {
- message: Some(Message::Item(4)),
- },
- ));
- p.state = CustomerState::Waiting { chair: *chair };
- }
- }
- CustomerState::Waiting { chair } => {
- debug!("waiting")
- }
- }
- }
- }
-}
-
-pub fn select_chair(chairs: &mut HashMap<IVec2, bool>) -> IVec2 {
- use rand::seq::IteratorRandom;
- let (chosen, free) = chairs
- .iter_mut()
- .filter(|(_p, free)| **free)
- .choose(&mut thread_rng())
- .unwrap();
- *free = false;
- *chosen
-}
-
-pub fn find_path(map: &HashSet<IVec2>, from: IVec2, to: IVec2) -> Option<Vec<Vec2>> {
- #[derive(Debug, PartialEq, Eq)]
- struct Open(i32, IVec2, IVec2);
- impl PartialOrd for Open {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- self.0.partial_cmp(&other.0)
- }
- }
- impl Ord for Open {
- fn cmp(&self, other: &Self) -> Ordering {
- self.0.cmp(&other.0)
- }
- }
-
- let mut visited = HashMap::new();
- let mut open = BinaryHeap::new();
- open.push(Open(1, from, from));
-
- loop {
- let Some(Open(_, p, f)) = open.pop() else {
- eprintln!("{visited:?}");
- return None;
- };
- if visited.contains_key(&p) {
- continue;
- }
- visited.insert(p, f);
- if p == to {
- break;
- }
- for d in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] {
- let n = p + d;
- if map.contains(&n) {
- open.push(Open(-d.distance_squared(to), n, p));
- }
- }
- }
-
- let mut path = Vec::new();
- let mut c = to;
- loop {
- path.push(c.as_vec2() + 0.5);
- let cn = visited[&c];
- if cn == c {
- break;
- }
- c = cn
- }
- Some(path)
-}
-
-fn move_player(p: &mut Customer, map: &HashSet<IVec2>, direction: Vec2, dt: f32) -> PacketS {
- let direction = direction.normalize_or_zero();
- if direction.length() > 0.1 {
- p.facing = direction + (p.facing - direction) * (-dt * 10.).exp();
- }
- let rot = p.facing.x.atan2(p.facing.y);
- p.vel += direction * dt * 0.5;
- p.position += p.vel;
- p.vel = p.vel * (-dt * 5.).exp();
- collide_player(p, map);
- PacketS::Position {
- pos: p.position,
- rot,
- }
-}
-
-const PLAYER_SIZE: f32 = 0.4;
-fn collide_player(p: &mut Customer, map: &HashSet<IVec2>) {
- for xo in -1..=1 {
- for yo in -1..=1 {
- let tile = IVec2::new(xo, yo) + p.position.as_ivec2();
- if map.contains(&tile) {
- continue;
- }
- let tile = tile.as_vec2();
- let d = aabb_circle_distance(tile, tile + Vec2::ONE, p.position);
- if d > PLAYER_SIZE {
- continue;
- }
- let h = 0.01;
- let d_sample_x =
- aabb_circle_distance(tile, tile + Vec2::ONE, p.position + Vec2::new(h, 0.));
- let d_sample_y =
- aabb_circle_distance(tile, tile + Vec2::ONE, p.position + Vec2::new(0., h));
- let grad = (Vec2::new(d_sample_x, d_sample_y) - d) / h;
-
- p.position += (PLAYER_SIZE - d) * grad;
- p.vel -= grad * grad.dot(p.vel);
- }
- }
-}
-fn aabb_circle_distance(min: Vec2, max: Vec2, p: Vec2) -> f32 {
- (p - p.clamp(min, max)).length()
-}
diff --git a/server/src/customer/mod.rs b/server/src/customer/mod.rs
new file mode 100644
index 00000000..ab3accdd
--- /dev/null
+++ b/server/src/customer/mod.rs
@@ -0,0 +1,201 @@
+pub mod movement;
+mod pathfinding;
+
+use crate::{
+ data::Gamedata,
+ game::Game,
+ protocol::{ItemIndex, Message, PacketC, PacketS, PlayerID},
+};
+use glam::{IVec2, Vec2};
+use log::{debug, error};
+use movement::MovementBase;
+use pathfinding::{find_path, Path};
+use rand::thread_rng;
+use std::{
+ collections::{HashMap, HashSet},
+ sync::Arc,
+ time::Duration,
+};
+use tokio::{
+ sync::{broadcast, RwLock},
+ time::interval,
+};
+
+struct DemandState {
+ data: Gamedata,
+ walkable: HashSet<IVec2>,
+ chairs: HashMap<IVec2, bool>,
+ items: HashMap<IVec2, ItemIndex>,
+ customers: HashMap<PlayerID, Customer>,
+}
+
+enum CustomerState {
+ WalkingToChair { path: Path, chair: IVec2 },
+ Waiting { chair: IVec2, demand: ItemIndex },
+ Exiting { path: Path },
+}
+
+struct Customer {
+ movement: MovementBase,
+ state: CustomerState,
+}
+
+pub async fn customer(game: Arc<RwLock<Game>>, mut grx: broadcast::Receiver<PacketC>) {
+ let mut state = DemandState {
+ walkable: Default::default(),
+ chairs: Default::default(),
+ items: Default::default(),
+ customers: Default::default(),
+ data: Gamedata::default(),
+ };
+ let initial = game.write().await.prime_client(-1);
+ for p in initial {
+ match p {
+ PacketC::Init { data, .. } => {
+ state.data = data;
+ }
+ PacketC::UpdateMap { pos, tile, .. } => {
+ let tilename = &state.data.tile_names[tile];
+ if tilename == "floor" || tilename == "door" || tilename == "chair" {
+ state.walkable.insert(pos);
+ }
+ if tilename == "chair" {
+ state.chairs.insert(pos, true);
+ }
+ }
+ _ => (),
+ }
+ }
+
+ let mut interval = interval(Duration::from_millis(40));
+ let mut packets_out = Vec::new();
+ loop {
+ tokio::select! {
+ packet = grx.recv() => {
+ match packet.unwrap() {
+ PacketC::PutItem { .. }
+ | PacketC::TakeItem { .. }
+ | PacketC::ProduceItem { .. }
+ | PacketC::ConsumeItem { .. } => {
+ let g = game.read().await;
+ update_items(&mut state, &g)
+ },
+ _ => ()
+ }
+ }
+ _ = interval.tick() => {
+ state.tick(&mut packets_out, 0.04);
+ for (player,packet) in packets_out.drain(..) {
+ if let Err(e) = game.write().await.packet_in(player, packet) {
+ error!("customer misbehaved: {e}")
+ }
+ }
+ }
+ }
+ }
+}
+
+fn update_items(state: &mut DemandState, game: &Game) {
+ state.items.clear();
+ for (&pos, tile) in game.tiles() {
+ if let Some(item) = &tile.item {
+ state.items.insert(pos, item.kind);
+ }
+ }
+}
+
+impl DemandState {
+ pub fn tick(&mut self, packets_out: &mut Vec<(PlayerID, PacketS)>, dt: f32) {
+ if self.customers.is_empty() {
+ let id = -1;
+ packets_out.push((
+ id,
+ PacketS::Join {
+ name: "George".to_string(),
+ character: 0,
+ },
+ ));
+ let chair = select_chair(&mut self.chairs);
+ let path = find_path(&self.walkable, self.data.customer_spawn.as_ivec2(), chair)
+ .expect("no path");
+ self.customers.insert(
+ id,
+ Customer {
+ movement: MovementBase {
+ position: self.data.customer_spawn,
+ facing: Vec2::X,
+ vel: Vec2::ZERO,
+ },
+ state: CustomerState::WalkingToChair { path, chair },
+ },
+ );
+ }
+ let mut customers_to_remove = Vec::new();
+ for (&id, p) in &mut self.customers {
+ match &mut p.state {
+ CustomerState::WalkingToChair { path, chair } => {
+ packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt)));
+ if path.is_done() {
+ let demand = self.data.get_item("tomato").unwrap();
+ packets_out.push((
+ id,
+ PacketS::Communicate {
+ message: Some(Message::Item(demand)),
+ },
+ ));
+ p.state = CustomerState::Waiting {
+ chair: *chair,
+ demand,
+ };
+ }
+ }
+ CustomerState::Waiting { chair, demand } => {
+ let demand_pos = [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y]
+ .into_iter()
+ .find_map(|off| {
+ let pos = *chair + off;
+ if self.items.get(&pos) == Some(demand) {
+ Some(pos)
+ } else {
+ None
+ }
+ });
+ if let Some(pos) = demand_pos {
+ if self.items.get(&pos) == Some(demand) {
+ packets_out.push((id, PacketS::Communicate { message: None }));
+ let path = find_path(
+ &self.walkable,
+ p.movement.position.as_ivec2(),
+ self.data.customer_spawn.as_ivec2(),
+ )
+ .expect("no path to exit");
+ p.state = CustomerState::Exiting { path }
+ }
+ }
+ debug!("waiting")
+ }
+ CustomerState::Exiting { path } => {
+ packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt)));
+ if path.is_done() {
+ packets_out.push((id, PacketS::Leave));
+ customers_to_remove.push(id);
+ }
+ }
+ }
+ }
+ for c in customers_to_remove {
+ self.customers.remove(&c).unwrap();
+ }
+ }
+}
+
+pub fn select_chair(chairs: &mut HashMap<IVec2, bool>) -> IVec2 {
+ use rand::seq::IteratorRandom;
+ let (chosen, free) = chairs
+ .iter_mut()
+ .filter(|(_p, free)| **free)
+ .choose(&mut thread_rng())
+ .unwrap();
+ *free = false;
+ *chosen
+}
diff --git a/server/src/customer/movement.rs b/server/src/customer/movement.rs
new file mode 100644
index 00000000..0ddabd0b
--- /dev/null
+++ b/server/src/customer/movement.rs
@@ -0,0 +1,56 @@
+use crate::protocol::PacketS;
+use glam::{IVec2, Vec2};
+use std::collections::HashSet;
+
+pub struct MovementBase {
+ pub position: Vec2,
+ pub facing: Vec2,
+ pub vel: Vec2,
+}
+
+impl MovementBase {
+ pub fn update(&mut self, map: &HashSet<IVec2>, direction: Vec2, dt: f32) -> PacketS {
+ let direction = direction.normalize_or_zero();
+ if direction.length() > 0.1 {
+ self.facing = direction + (self.facing - direction) * (-dt * 10.).exp();
+ }
+ let rot = self.facing.x.atan2(self.facing.y);
+ self.vel += direction * dt * 0.5;
+ self.position += self.vel;
+ self.vel = self.vel * (-dt * 5.).exp();
+ collide_player(self, map);
+ PacketS::Position {
+ pos: self.position,
+ rot,
+ }
+ }
+}
+
+const PLAYER_SIZE: f32 = 0.4;
+pub fn collide_player(p: &mut MovementBase, map: &HashSet<IVec2>) {
+ for xo in -1..=1 {
+ for yo in -1..=1 {
+ let tile = IVec2::new(xo, yo) + p.position.as_ivec2();
+ if map.contains(&tile) {
+ continue;
+ }
+ let tile = tile.as_vec2();
+ let d = aabb_circle_distance(tile, tile + Vec2::ONE, p.position);
+ if d > PLAYER_SIZE {
+ continue;
+ }
+ let h = 0.01;
+ let d_sample_x =
+ aabb_circle_distance(tile, tile + Vec2::ONE, p.position + Vec2::new(h, 0.));
+ let d_sample_y =
+ aabb_circle_distance(tile, tile + Vec2::ONE, p.position + Vec2::new(0., h));
+ let grad = (Vec2::new(d_sample_x, d_sample_y) - d) / h;
+
+ p.position += (PLAYER_SIZE - d) * grad;
+ p.vel -= grad * grad.dot(p.vel);
+ }
+ }
+}
+pub fn aabb_circle_distance(min: Vec2, max: Vec2, p: Vec2) -> f32 {
+ (p - p.clamp(min, max)).length()
+}
diff --git a/server/src/customer/pathfinding.rs b/server/src/customer/pathfinding.rs
new file mode 100644
index 00000000..d25c6913
--- /dev/null
+++ b/server/src/customer/pathfinding.rs
@@ -0,0 +1,83 @@
+use super::movement::MovementBase;
+use crate::protocol::PacketS;
+use glam::{IVec2, Vec2};
+use log::debug;
+use std::{
+ cmp::Ordering,
+ collections::{BinaryHeap, HashMap, HashSet},
+};
+
+pub struct Path(Vec<Vec2>);
+
+impl Path {
+ pub fn execute_tick(
+ &mut self,
+ customer: &mut MovementBase,
+ walkable: &HashSet<IVec2>,
+ dt: f32,
+ ) -> PacketS {
+ if let Some(next) = self.0.last().copied() {
+ debug!("next {next}");
+ if next.distance(customer.position) < if self.0.len() == 1 { 0.1 } else { 0.6 } {
+ self.0.pop();
+ }
+ customer.update(&walkable, next - customer.position, dt)
+ } else {
+ customer.update(&walkable, Vec2::ZERO, dt)
+ }
+ }
+ pub fn is_done(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+pub fn find_path(map: &HashSet<IVec2>, from: IVec2, to: IVec2) -> Option<Path> {
+ #[derive(Debug, PartialEq, Eq)]
+ struct Open(i32, IVec2, IVec2);
+ impl PartialOrd for Open {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ self.0.partial_cmp(&other.0)
+ }
+ }
+ impl Ord for Open {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.0.cmp(&other.0)
+ }
+ }
+
+ let mut visited = HashMap::new();
+ let mut open = BinaryHeap::new();
+ open.push(Open(1, from, from));
+
+ loop {
+ let Some(Open(_, p, f)) = open.pop() else {
+ eprintln!("{visited:?}");
+ return None;
+ };
+ if visited.contains_key(&p) {
+ continue;
+ }
+ visited.insert(p, f);
+ if p == to {
+ break;
+ }
+ for d in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] {
+ let n = p + d;
+ if map.contains(&n) {
+ open.push(Open(-d.distance_squared(to), n, p));
+ }
+ }
+ }
+
+ let mut path = Vec::new();
+ let mut c = to;
+ loop {
+ path.push(c.as_vec2() + 0.5);
+ let cn = visited[&c];
+ if cn == c {
+ break;
+ }
+ c = cn
+ }
+ Some(Path(path))
+}
diff --git a/server/src/data.rs b/server/src/data.rs
index 9f03b1ea..d138f360 100644
--- a/server/src/data.rs
+++ b/server/src/data.rs
@@ -123,6 +123,9 @@ impl Gamedata {
pub fn get_tile(&self, name: &str) -> Option<TileIndex> {
self.tile_names.iter().position(|t| t == name)
}
+ pub fn get_item(&self, name: &str) -> Option<TileIndex> {
+ self.item_names.iter().position(|t| t == name)
+ }
}
impl Action {
pub fn duration(&self) -> f32 {