aboutsummaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-06-25 23:34:10 +0200
committermetamuffin <metamuffin@disroot.org>2024-06-25 23:34:10 +0200
commit4dc15a1e86ef1ae985fdf36f1a84d07b1de99ea7 (patch)
tree6a262cd2be9abee480adda3e367c7f8abf8845d6 /server
parent84c90e84a1e0d6cd2eae36fd8888354b4e23c354 (diff)
downloadhurrycurry-4dc15a1e86ef1ae985fdf36f1a84d07b1de99ea7.tar
hurrycurry-4dc15a1e86ef1ae985fdf36f1a84d07b1de99ea7.tar.bz2
hurrycurry-4dc15a1e86ef1ae985fdf36f1a84d07b1de99ea7.tar.zst
server can change map at runtime
Diffstat (limited to 'server')
-rw-r--r--server/Cargo.toml2
-rw-r--r--server/src/bin/graph.rs63
-rw-r--r--server/src/customer/mod.rs78
-rw-r--r--server/src/data.rs233
-rw-r--r--server/src/game.rs52
-rw-r--r--server/src/lib.rs21
-rw-r--r--server/src/main.rs42
-rw-r--r--server/src/protocol.rs16
-rw-r--r--server/src/state.rs68
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(())
+ }
+}