diff options
Diffstat (limited to 'server')
| -rw-r--r-- | server/Cargo.toml | 2 | ||||
| -rw-r--r-- | server/src/bin/graph.rs | 63 | ||||
| -rw-r--r-- | server/src/customer/mod.rs | 78 | ||||
| -rw-r--r-- | server/src/data.rs | 233 | ||||
| -rw-r--r-- | server/src/game.rs | 52 | ||||
| -rw-r--r-- | server/src/lib.rs | 21 | ||||
| -rw-r--r-- | server/src/main.rs | 42 | ||||
| -rw-r--r-- | server/src/protocol.rs | 16 | ||||
| -rw-r--r-- | server/src/state.rs | 68 | 
9 files changed, 370 insertions, 205 deletions
| diff --git a/server/Cargo.toml b/server/Cargo.toml index 23a34d99..9b954908 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -16,3 +16,5 @@ tokio-tungstenite = "0.23.1"  futures-util = "0.3.30"  serde_yaml = "0.9.34+deprecated"  rand = "0.9.0-alpha.1" +shlex = "1.3.0" +clap = { version = "4.5.7", features = ["derive"] } 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 <https://www.gnu.org/licenses/>. -     +  */ +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 <https://www.gnu.org/licenses/>. -     +  */  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<IVec2>,      chairs: HashMap<IVec2, bool>,      items: HashMap<IVec2, ItemIndex>, @@ -77,34 +79,21 @@ struct Customer {      state: CustomerState,  } -pub async fn customer(game: Arc<RwLock<Game>>, mut grx: broadcast::Receiver<PacketC>) { +pub async fn customer(gstate: Arc<RwLock<State>>, mut grx: broadcast::Receiver<PacketC>) {      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<RwLock<Game>>, mut grx: broadcast::Receiver<Pack      loop {          tokio::select! {              packet = grx.recv() => { -                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<RwLock<Game>>, mut grx: broadcast::Receiver<Pack      }  } +// TODO very inefficient, please do that incrementally  fn update_items(state: &mut CustomerManager, game: &Game) {      state.items.clear();      for (&pos, tile) in game.tiles() { @@ -150,12 +144,36 @@ impl DemandState {      }      pub fn generate_demand(&self) -> DemandIndex {          // TODO insert sofa magic formula -          DemandIndex(random::<usize>() % 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<Recipe>, +    #[serde(skip)] +    pub recipes: Vec<Recipe>, +    #[serde(skip)]      pub demands: Vec<Demand>,      pub item_names: Vec<String>,      pub tile_names: Vec<String>, @@ -90,105 +97,153 @@ pub struct Gamedata {      pub customer_spawn: Vec2,  } -pub fn build_gamedata( -    recipes_in: Vec<RecipeDecl>, -    map_in: InitialMap, -    demands_in: Vec<DemandDecl>, -) -> 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(); +#[derive(Debug, Deserialize, Default)] +pub struct DataIndex { +    pub maps: HashSet<String>, +    pub demands: HashSet<String>, +    pub recipes: HashSet<String>, +} + +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<Gamedata> { +        let [demands, map, recipes] = spec +            .split("-") +            .collect::<Vec<_>>() +            .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<RecipeDecl>, +        map_in: InitialMap, +        demands_in: Vec<DemandDecl>, +    ) -> Result<Self> { +        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 { +        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, -                    inputs: [inputs.next(), inputs.next()], +                    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 { +                        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 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(); +        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, +        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<Gamedata>) -> 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::<HashMap<_, _>>(); + +        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<IVec2, Tile> { @@ -97,10 +126,9 @@ impl Game {          self.packet_out.pop_front()      } -    pub fn prime_client(&self, id: PlayerID) -> Vec<PacketC> { +    pub fn prime_client(&self) -> Vec<PacketC> {          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 <https://www.gnu.org/licenses/>. -     -*/ -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 <https://www.gnu.org/licenses/>. -     +  */  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::<PacketC>(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::<PacketC>(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 <https://www.gnu.org/licenses/>. -     +  */  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<TileIndex>,          neighbors: [Option<TileIndex>; 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<PacketC>, +    pub game: Game, +} + +#[derive(Parser)] +#[clap(multicall = true)] +enum Command { +    Start { spec: String }, +} + +impl State { +    pub fn new(tx: Sender<PacketC>) -> Result<Self> { +        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(()) +    } +} | 
