From 4dc15a1e86ef1ae985fdf36f1a84d07b1de99ea7 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Tue, 25 Jun 2024 23:34:10 +0200 Subject: server can change map at runtime --- server/src/bin/graph.rs | 63 ++++++------ server/src/customer/mod.rs | 78 +++++++++------ server/src/data.rs | 239 ++++++++++++++++++++++++++++----------------- server/src/game.rs | 52 +++++++--- server/src/lib.rs | 21 ++-- server/src/main.rs | 42 ++++---- server/src/protocol.rs | 16 +-- server/src/state.rs | 68 +++++++++++++ 8 files changed, 371 insertions(+), 208 deletions(-) create mode 100644 server/src/state.rs (limited to 'server/src') diff --git a/server/src/bin/graph.rs b/server/src/bin/graph.rs index aa65e91a..94dddbc4 100644 --- a/server/src/bin/graph.rs +++ b/server/src/bin/graph.rs @@ -1,41 +1,46 @@ /* Undercooked - a game about cooking Copyright 2024 metamuffin - + 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 . - + */ +use anyhow::Result; use undercooked::{ + data::DataIndex, interaction::Recipe, - load_gamedata, protocol::{ItemIndex, RecipeIndex}, }; -fn main() { - let data = load_gamedata(); +fn main() -> Result<()> { + let mut index = DataIndex::default(); + index.reload()?; println!("digraph {{"); - for i in 0..data.item_names.len() { - println!("i{i} [label=\"{}\"]", data.item_name(ItemIndex(i))) - } - for (RecipeIndex(ri), recipe) in data.recipes() { - let (kind, color) = match recipe { - Recipe::Passive { .. } => ("Passive", "#2bc493"), - Recipe::Active { .. } => ("Active", "#47c42b"), - Recipe::Instant { .. } => ("Instant", "#5452d8"), - }; - println!( + for rn in &index.recipes { + let data = index.generate(format!("default-none-{rn}"))?; + + for i in 0..data.item_names.len() { + println!("i{i} [label=\"{}\"]", data.item_name(ItemIndex(i))) + } + for (RecipeIndex(ri), recipe) in data.recipes() { + let (kind, color) = match recipe { + Recipe::Passive { .. } => ("Passive", "#2bc493"), + Recipe::Active { .. } => ("Active", "#47c42b"), + Recipe::Instant { .. } => ("Instant", "#5452d8"), + }; + println!( "r{ri} [label=\"{kind}\\non {}\" shape=box color={color:?} fillcolor={color:?} style=filled]", if let Some(tile) = recipe.tile() { data.tile_name(tile) @@ -43,23 +48,25 @@ fn main() { "anything" } ); - for ItemIndex(input) in recipe.inputs() { - println!("i{input} -> r{ri}") - } - for ItemIndex(output) in recipe.outputs() { - println!("r{ri} -> i{output}") + for ItemIndex(input) in recipe.inputs() { + println!("i{input} -> r{ri}") + } + for ItemIndex(output) in recipe.outputs() { + println!("r{ri} -> i{output}") + } } - } - for (di, d) in data.demands.iter().enumerate() { - let color = "#c4422b"; - println!( + for (di, d) in data.demands.iter().enumerate() { + let color = "#c4422b"; + println!( "d{di} [label=\"Demand\\ntakes {}s\" shape=box color={color:?} fillcolor={color:?} style=filled]", d.duration ); - println!("i{} -> d{di}", d.from.0); - println!("d{di} -> i{}", d.to.0); + println!("i{} -> d{di}", d.from.0); + println!("d{di} -> i{}", d.to.0); + } } println!("}}"); + Ok(()) } diff --git a/server/src/customer/mod.rs b/server/src/customer/mod.rs index 16275227..185133e7 100644 --- a/server/src/customer/mod.rs +++ b/server/src/customer/mod.rs @@ -1,19 +1,19 @@ /* Undercooked - a game about cooking Copyright 2024 metamuffin - + 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 . - + */ pub mod movement; mod pathfinding; @@ -22,6 +22,7 @@ use crate::{ data::Gamedata, game::Game, protocol::{DemandIndex, ItemIndex, Message, PacketC, PacketS, PlayerID}, + state::State, }; use glam::{IVec2, Vec2}; use log::{debug, error}; @@ -39,6 +40,7 @@ use tokio::{ }; struct CustomerManager { + disabled: bool, walkable: HashSet, chairs: HashMap, items: HashMap, @@ -77,34 +79,21 @@ struct Customer { state: CustomerState, } -pub async fn customer(game: Arc>, mut grx: broadcast::Receiver) { +pub async fn customer(gstate: Arc>, mut grx: broadcast::Receiver) { let mut state = CustomerManager { customer_id_counter: PlayerID(0), walkable: Default::default(), chairs: Default::default(), items: Default::default(), customers: Default::default(), + disabled: true, demand: DemandState { data: Gamedata::default(), }, }; - let initial = game.write().await.prime_client(PlayerID(-1)); - for p in initial { - match p { - PacketC::Init { data, .. } => { - state.demand.data = data; - } - PacketC::UpdateMap { pos, tile, .. } => { - let tilename = state.demand.data.tile_name(tile); - if !state.demand.data.is_tile_colliding(tile) { - state.walkable.insert(pos); - } - if tilename == "chair" { - state.chairs.insert(pos, true); - } - } - _ => (), - } + let initial = gstate.write().await.game.prime_client(); + for packet in initial { + state.packet(packet); } let mut interval = interval(Duration::from_millis(40)); @@ -112,21 +101,25 @@ pub async fn customer(game: Arc>, mut grx: broadcast::Receiver { - match packet.unwrap() { + let packet = packet.unwrap(); + match packet { PacketC::PutItem { .. } | PacketC::TakeItem { .. } | PacketC::SetTileItem { .. } => { - let g = game.read().await; - update_items(&mut state, &g) + let g = gstate.read().await; + update_items(&mut state, &g.game) }, _ => () } + state.packet(packet); } _ = 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}") + if !state.disabled { + state.tick(&mut packets_out, 0.04); + for (player,packet) in packets_out.drain(..) { + if let Err(e) = gstate.write().await.packet_in(player, packet).await { + error!("customer misbehaved: {e}") + } } } } @@ -134,6 +127,7 @@ pub async fn customer(game: Arc>, mut grx: broadcast::Receiver DemandIndex { // TODO insert sofa magic formula - DemandIndex(random::() % self.data.demands.len()) } } impl CustomerManager { + pub fn packet(&mut self, packet: PacketC) { + match packet { + PacketC::Data { data } => { + self.disabled = data.demands.is_empty(); + self.demand.data = data; + } + PacketC::RemovePlayer { id } => { + self.customers.remove(&id); + } + PacketC::UpdateMap { + tile: pos, + kind: Some(tile), + .. + } => { + let tilename = self.demand.data.tile_name(tile); + if !self.demand.data.is_tile_colliding(tile) { + self.walkable.insert(pos); + } + if tilename == "chair" { + self.chairs.insert(pos, true); + } + } + _ => (), + } + } pub fn tick(&mut self, packets_out: &mut Vec<(PlayerID, PacketS)>, dt: f32) { if self.customers.len() < self.demand.target_customer_count() { self.customer_id_counter.0 -= 1; diff --git a/server/src/data.rs b/server/src/data.rs index 64509f37..e980ccbd 100644 --- a/server/src/data.rs +++ b/server/src/data.rs @@ -19,9 +19,14 @@ use crate::{ interaction::Recipe, protocol::{DemandIndex, ItemIndex, RecipeIndex, TileIndex}, }; +use anyhow::{anyhow, bail, Result}; use glam::{IVec2, Vec2}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, sync::RwLock}; +use std::{ + collections::{HashMap, HashSet}, + fs::File, + sync::RwLock, +}; #[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] #[serde(rename_all = "snake_case")] @@ -78,7 +83,9 @@ pub struct Demand { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Gamedata { - recipes: Vec, + #[serde(skip)] + pub recipes: Vec, + #[serde(skip)] pub demands: Vec, pub item_names: Vec, pub tile_names: Vec, @@ -90,105 +97,153 @@ pub struct Gamedata { pub customer_spawn: Vec2, } -pub fn build_gamedata( - recipes_in: Vec, - map_in: InitialMap, - demands_in: Vec, -) -> Gamedata { - let item_names = RwLock::new(Vec::new()); - let tile_names = RwLock::new(Vec::new()); - let mut recipes = Vec::new(); - let mut demands = Vec::new(); - - for r in recipes_in { - let r2 = r.clone(); - let mut inputs = r - .inputs - .into_iter() - .map(|i| ItemIndex(register(&item_names, i))); - let mut outputs = r - .outputs - .into_iter() - .map(|o| ItemIndex(register(&item_names, o))); - let tile = r.tile.map(|t| TileIndex(register(&tile_names, t))); - match r.action { - Action::Never => {} - Action::Passive => recipes.push(Recipe::Passive { - duration: r.duration.expect("duration for passive missing"), - warn: r.warn, - tile, - revert_duration: r.revert_duration, - input: inputs.next().expect("passive recipe without input"), - output: outputs.next(), - }), - Action::Active => recipes.push(Recipe::Active { - duration: r.duration.expect("duration for active missing"), - tile, - input: inputs.next().expect("active recipe without input"), - outputs: [outputs.next(), outputs.next()], - }), - Action::Instant => { - recipes.push(Recipe::Instant { +#[derive(Debug, Deserialize, Default)] +pub struct DataIndex { + pub maps: HashSet, + pub demands: HashSet, + pub recipes: HashSet, +} + +impl DataIndex { + pub fn reload(&mut self) -> anyhow::Result<()> { + *self = serde_yaml::from_reader(File::open("data/index.yaml")?)?; + Ok(()) + } + + pub fn generate(&self, spec: String) -> anyhow::Result { + let [demands, map, recipes] = spec + .split("-") + .collect::>() + .try_into() + .map_err(|_| anyhow!("data specification malformed"))?; + + if !self.demands.contains(demands) { + bail!("unknown demands: {demands:?}"); + } + if !self.maps.contains(map) { + bail!("unknown map: {map:?}"); + } + if !self.recipes.contains(recipes) { + bail!("unknown recipes: {recipes:?}"); + } + + let demands_path = format!("data/demands/{demands}.yaml"); + let map_path = format!("data/maps/{map}.yaml"); + let recipes_path = format!("data/recipes/{recipes}.yaml"); + + let demands_in = serde_yaml::from_reader(File::open(demands_path).unwrap()).unwrap(); + let map_in = serde_yaml::from_reader(File::open(map_path).unwrap()).unwrap(); + let recipes_in = serde_yaml::from_reader(File::open(recipes_path).unwrap()).unwrap(); + + Ok(Gamedata::build(recipes_in, map_in, demands_in)?) + } +} + +impl Gamedata { + pub fn build( + recipes_in: Vec, + map_in: InitialMap, + demands_in: Vec, + ) -> Result { + let item_names = RwLock::new(Vec::new()); + let tile_names = RwLock::new(Vec::new()); + let mut recipes = Vec::new(); + let mut demands = Vec::new(); + + for r in recipes_in { + let r2 = r.clone(); + let mut inputs = r + .inputs + .into_iter() + .map(|i| ItemIndex(register(&item_names, i))); + let mut outputs = r + .outputs + .into_iter() + .map(|o| ItemIndex(register(&item_names, o))); + let tile = r.tile.map(|t| TileIndex(register(&tile_names, t))); + match r.action { + Action::Never => {} + Action::Passive => recipes.push(Recipe::Passive { + duration: r.duration.expect("duration for passive missing"), + warn: r.warn, + tile, + revert_duration: r.revert_duration, + input: inputs.next().expect("passive recipe without input"), + output: outputs.next(), + }), + Action::Active => recipes.push(Recipe::Active { + duration: r.duration.expect("duration for active missing"), tile, - inputs: [inputs.next(), inputs.next()], + input: inputs.next().expect("active recipe without input"), outputs: [outputs.next(), outputs.next()], - }); + }), + Action::Instant => { + recipes.push(Recipe::Instant { + tile, + inputs: [inputs.next(), inputs.next()], + outputs: [outputs.next(), outputs.next()], + }); + } } + assert_eq!(inputs.next(), None, "{r2:?}"); + assert_eq!(outputs.next(), None, "{r2:?}"); } - assert_eq!(inputs.next(), None, "{r2:?}"); - assert_eq!(outputs.next(), None, "{r2:?}"); - } - for d in demands_in { - demands.push(Demand { - from: ItemIndex(register(&item_names, d.from)), - to: ItemIndex(register(&item_names, d.to)), - duration: d.duration, - }) - } + for d in demands_in { + demands.push(Demand { + from: ItemIndex(register(&item_names, d.from)), + to: ItemIndex(register(&item_names, d.to)), + duration: d.duration, + }) + } - let mut chef_spawn = Vec2::new(0., 0.); - let mut customer_spawn = Vec2::new(0., 0.); - let mut initial_map = HashMap::new(); - for (y, line) in map_in.map.iter().enumerate() { - for (x, tile) in line.trim().chars().enumerate() { - let pos = IVec2::new(x as i32, y as i32); - if tile == map_in.chef_spawn { - chef_spawn = pos.as_vec2() + Vec2::splat(0.5); + let mut chef_spawn = Vec2::new(0., 0.); + let mut customer_spawn = Vec2::new(0., 0.); + let mut initial_map = HashMap::new(); + for (y, line) in map_in.map.iter().enumerate() { + for (x, tile) in line.trim().chars().enumerate() { + let pos = IVec2::new(x as i32, y as i32); + if tile == map_in.chef_spawn { + chef_spawn = pos.as_vec2() + Vec2::splat(0.5); + } + if tile == map_in.customer_spawn { + customer_spawn = pos.as_vec2() + Vec2::splat(0.5); + } + let tilename = map_in + .tiles + .get(&tile) + .ok_or(anyhow!("tile {tile} is undefined"))? + .clone(); + let itemname = map_in.items.get(&tile).cloned(); + let tile = TileIndex(register(&tile_names, tilename)); + let item = itemname.map(|i| ItemIndex(register(&item_names, i))); + initial_map.insert(pos, (tile, item)); } - if tile == map_in.customer_spawn { - customer_spawn = pos.as_vec2() + Vec2::splat(0.5); - } - let tilename = map_in.tiles[&tile].clone(); - let itemname = map_in.items.get(&tile).cloned(); - let tile = TileIndex(register(&tile_names, tilename)); - let item = itemname.map(|i| ItemIndex(register(&item_names, i))); - initial_map.insert(pos, (tile, item)); } - } - let item_names = item_names.into_inner().unwrap(); - let tile_names = tile_names.into_inner().unwrap(); - - let tile_collide = tile_names - .iter() - .map(|i| !map_in.walkable.contains(i)) - .collect(); - let tile_interact = tile_names - .iter() - .map(|i| !map_in.collider.contains(i) && !map_in.walkable.contains(i)) - .collect(); - - Gamedata { - demands, - tile_collide, - tile_interact, - recipes, - initial_map, - item_names, - tile_names, - chef_spawn, - customer_spawn, + let item_names = item_names.into_inner().unwrap(); + let tile_names = tile_names.into_inner().unwrap(); + + let tile_collide = tile_names + .iter() + .map(|i| !map_in.walkable.contains(i)) + .collect(); + let tile_interact = tile_names + .iter() + .map(|i| !map_in.collider.contains(i) && !map_in.walkable.contains(i)) + .collect(); + + Ok(Gamedata { + demands, + tile_collide, + tile_interact, + recipes, + initial_map, + item_names, + tile_names, + chef_spawn, + customer_spawn, + }) } } diff --git a/server/src/game.rs b/server/src/game.rs index c5eb8c9f..c0a03616 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -67,15 +67,39 @@ pub struct Game { } impl Game { - pub fn new(gamedata: Arc) -> Self { - let mut g = Self { - data: gamedata.clone(), + pub fn new() -> Self { + Self { + data: Gamedata::default().into(), packet_out: Default::default(), players: Default::default(), tiles: Default::default(), - }; - for (&p, (tile, item)) in &gamedata.initial_map { - g.tiles.insert( + } + } + + fn unload(&mut self) { + for (id, _) in self.players.drain() { + self.packet_out.push_back(PacketC::RemovePlayer { id }) + } + for (pos, _) in self.tiles.drain() { + self.packet_out.push_back(PacketC::UpdateMap { + tile: pos, + kind: None, + neighbors: [None, None, None, None], + }) + } + } + pub fn load(&mut self, gamedata: Gamedata) { + let players = self + .players + .iter() + .map(|(id, p)| (*id, (p.name.to_owned(), p.character))) + .collect::>(); + + self.unload(); + + self.data = gamedata.into(); + for (&p, (tile, item)) in &self.data.initial_map { + self.tiles.insert( p, Tile { kind: *tile, @@ -86,7 +110,12 @@ impl Game { }, ); } - g + for (id, (name, character)) in players { + self.packet_in(id, PacketS::Join { name, character }) + .unwrap(); + } + + self.packet_out.extend(self.prime_client()); } pub fn tiles(&self) -> &HashMap { @@ -97,10 +126,9 @@ impl Game { self.packet_out.pop_front() } - pub fn prime_client(&self, id: PlayerID) -> Vec { + pub fn prime_client(&self) -> Vec { let mut out = Vec::new(); - out.push(PacketC::Init { - id, + out.push(PacketC::Data { data: self.data.deref().to_owned(), }); for (&id, player) in &self.players { @@ -125,14 +153,14 @@ impl Game { } for (&tile, tdata) in &self.tiles { out.push(PacketC::UpdateMap { - pos: tile, + tile, neighbors: [ self.tiles.get(&(tile + IVec2::NEG_Y)).map(|e| e.kind), self.tiles.get(&(tile + IVec2::NEG_X)).map(|e| e.kind), self.tiles.get(&(tile + IVec2::Y)).map(|e| e.kind), self.tiles.get(&(tile + IVec2::X)).map(|e| e.kind), ], - tile: tdata.kind.clone(), + kind: Some(tdata.kind.clone()), }); if let Some(item) = &tdata.item { out.push(PacketC::SetTileItem { diff --git a/server/src/lib.rs b/server/src/lib.rs index ac0fbfa4..466defb4 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,33 +1,24 @@ /* Undercooked - a game about cooking Copyright 2024 metamuffin - + 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 . - -*/ -use data::{build_gamedata, Gamedata}; -use std::fs::File; +*/ +#![feature(if_let_guard)] pub mod customer; pub mod data; pub mod game; pub mod interaction; pub mod protocol; - -pub fn load_gamedata() -> Gamedata { - build_gamedata( - serde_yaml::from_reader(File::open("data/recipes.yaml").unwrap()).unwrap(), - serde_yaml::from_reader(File::open("data/map.yaml").unwrap()).unwrap(), - serde_yaml::from_reader(File::open("data/demands.yaml").unwrap()).unwrap(), - ) -} +pub mod state; diff --git a/server/src/main.rs b/server/src/main.rs index aeda9c2f..6773bf29 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,19 +1,19 @@ /* Undercooked - a game about cooking Copyright 2024 metamuffin - + 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 . - + */ use anyhow::Result; use futures_util::{SinkExt, StreamExt}; @@ -23,14 +23,13 @@ use tokio::{ net::TcpListener, spawn, sync::{broadcast, mpsc::channel, RwLock}, - time::sleep, + time::interval, }; use tokio_tungstenite::tungstenite::Message; use undercooked::{ customer::customer, - game::Game, - load_gamedata, protocol::{PacketC, PacketS, PlayerID}, + state::State, }; #[tokio::main] @@ -39,29 +38,22 @@ async fn main() -> Result<()> { let ws_listener = TcpListener::bind("0.0.0.0:27032").await?; info!("listening for websockets on {}", ws_listener.local_addr()?); - let data = load_gamedata(); - let game = Arc::new(RwLock::new(Game::new(data.into()))); let (tx, rx) = broadcast::channel::(1024); + let state = Arc::new(RwLock::new(State::new(tx)?)); { - let game = game.clone(); + let state = state.clone(); spawn(async move { let dt = 1. / 25.; + let mut tick = interval(Duration::from_secs_f32(dt)); loop { - { - let mut g = game.write().await; - g.tick(dt); - while let Some(p) = g.packet_out() { - debug!("-> {p:?}"); - let _ = tx.send(p); - } - } - sleep(Duration::from_secs_f32(dt)).await; + tick.tick().await; + state.write().await.tick(dt).await; } }); } - spawn(customer(game.clone(), rx.resubscribe())); + spawn(customer(state.clone(), rx.resubscribe())); for id in (1..).map(PlayerID) { let (sock, addr) = ws_listener.accept().await?; @@ -70,11 +62,12 @@ async fn main() -> Result<()> { continue; }; let (mut write, mut read) = sock.split(); - let game = game.clone(); + let state = state.clone(); let mut rx = rx.resubscribe(); let (error_tx, mut error_rx) = channel::(8); info!("{addr} connected via ws"); - let init = game.write().await.prime_client(id); + let mut init = state.write().await.game.prime_client(); + init.insert(0, PacketC::Init { id }); spawn(async move { for p in init { if let Err(e) = write @@ -114,7 +107,7 @@ async fn main() -> Result<()> { break; }; debug!("<- {id:?} {packet:?}"); - if let Err(e) = game.write().await.packet_in(id, packet) { + if let Err(e) = state.write().await.packet_in(id, packet).await { warn!("client error: {e}"); let _ = error_tx .send(PacketC::Error { @@ -127,7 +120,8 @@ async fn main() -> Result<()> { _ => (), } } - let _ = game.write().await.packet_in(id, PacketS::Leave); + info!("{id:?} left"); + state.write().await.packet_in(id, PacketS::Leave).await.ok(); }); } Ok(()) diff --git a/server/src/protocol.rs b/server/src/protocol.rs index 8690febf..262fad8d 100644 --- a/server/src/protocol.rs +++ b/server/src/protocol.rs @@ -1,19 +1,19 @@ /* Undercooked - a game about cooking Copyright 2024 metamuffin - + 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 . - + */ use crate::data::Gamedata; use glam::{IVec2, Vec2}; @@ -77,9 +77,11 @@ pub enum Message { #[serde(rename_all = "snake_case", tag = "type")] pub enum PacketC { Init { - data: Gamedata, id: PlayerID, }, + Data { + data: Gamedata, + }, AddPlayer { id: PlayerID, position: Vec2, @@ -116,8 +118,8 @@ pub enum PacketC { warn: bool, }, UpdateMap { - pos: IVec2, - tile: TileIndex, + tile: IVec2, + kind: Option, neighbors: [Option; 4], }, Collide { diff --git a/server/src/state.rs b/server/src/state.rs new file mode 100644 index 00000000..98f92a24 --- /dev/null +++ b/server/src/state.rs @@ -0,0 +1,68 @@ +use crate::{ + data::DataIndex, + game::Game, + protocol::{Message, PacketC, PacketS, PlayerID}, +}; +use anyhow::{anyhow, Result}; +use clap::Parser; +use log::debug; +use tokio::sync::broadcast::Sender; + +pub struct State { + index: DataIndex, + tx: Sender, + pub game: Game, +} + +#[derive(Parser)] +#[clap(multicall = true)] +enum Command { + Start { spec: String }, +} + +impl State { + pub fn new(tx: Sender) -> Result { + let mut index = DataIndex::default(); + index.reload()?; + + let mut game = Game::new(); + game.load(index.generate("none-lobby-none".to_string())?); + + Ok(Self { game, index, tx }) + } + + pub async fn tick(&mut self, dt: f32) { + self.game.tick(dt); + while let Some(p) = self.game.packet_out() { + debug!("-> {p:?}"); + let _ = self.tx.send(p); + } + } + pub async fn packet_in(&mut self, player: PlayerID, packet: PacketS) -> Result<()> { + match &packet { + PacketS::Communicate { + message: Some(Message::Text(message)), + } if let Some(command) = message.strip_prefix("/") => { + self.handle_command(Command::try_parse_from( + shlex::split(command) + .ok_or(anyhow!("quoting invalid"))? + .into_iter(), + )?) + .await?; + return Ok(()); + } + _ => (), + } + self.game.packet_in(player, packet)?; + Ok(()) + } + + async fn handle_command(&mut self, command: Command) -> Result<()> { + match command { + Command::Start { spec } => { + self.game.load(self.index.generate(spec)?); + } + } + Ok(()) + } +} -- cgit v1.2.3-70-g09d2