diff options
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/customer.rs | 243 | ||||
-rw-r--r-- | server/src/customer/mod.rs | 201 | ||||
-rw-r--r-- | server/src/customer/movement.rs | 56 | ||||
-rw-r--r-- | server/src/customer/pathfinding.rs | 83 | ||||
-rw-r--r-- | server/src/data.rs | 3 |
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 { |