aboutsummaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-09-19 22:27:46 +0200
committermetamuffin <metamuffin@disroot.org>2025-09-19 22:40:39 +0200
commit402067b8317195fd2bc4ab4d92b5ace94fadb7c0 (patch)
tree2437c52ae71a11c4d17a6fa4597f8152dae96ddc /server
parent2f311fec691cd7a62fa4f95ee0419089913b5dd8 (diff)
downloadhurrycurry-402067b8317195fd2bc4ab4d92b5ace94fadb7c0.tar
hurrycurry-402067b8317195fd2bc4ab4d92b5ace94fadb7c0.tar.bz2
hurrycurry-402067b8317195fd2bc4ab4d92b5ace94fadb7c0.tar.zst
Refactor book part 1
Diffstat (limited to 'server')
-rw-r--r--server/protocol/src/book.rs70
-rw-r--r--server/protocol/src/helpers.rs12
-rw-r--r--server/protocol/src/lib.rs84
-rw-r--r--server/src/commands.rs105
-rw-r--r--server/src/data/mod.rs47
-rw-r--r--server/src/entity/book.rs2
-rw-r--r--server/src/main.rs23
-rw-r--r--server/src/scoreboard.rs36
-rw-r--r--server/src/server.rs7
-rw-r--r--server/src/state.rs11
-rw-r--r--server/tools/Cargo.toml14
-rw-r--r--server/tools/src/book.rs41
-rw-r--r--server/tools/src/diagram_layout.rs75
-rw-r--r--server/tools/src/graph.rs (renamed from server/src/bin/graph.rs)9
-rw-r--r--server/tools/src/graph_summary.rs (renamed from server/src/bin/graph_summary.rs)7
-rw-r--r--server/tools/src/main.rs46
-rw-r--r--server/tools/src/recipe_diagram.rs112
17 files changed, 491 insertions, 210 deletions
diff --git a/server/protocol/src/book.rs b/server/protocol/src/book.rs
new file mode 100644
index 00000000..5bcd0a22
--- /dev/null
+++ b/server/protocol/src/book.rs
@@ -0,0 +1,70 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright (C) 2025 Hurry Curry! Contributors
+
+ 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::Message;
+use bincode::{Decode, Encode};
+use glam::Vec2;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
+pub struct Book {
+ pub pages: Vec<BookPage>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
+#[serde(rename_all = "snake_case", tag = "page_type")]
+pub enum BookPage {
+ Cover,
+ Contents {
+ table: Vec<(Message, usize)>,
+ },
+ Text {
+ paragraphs: Vec<Message>,
+ },
+ Recipe {
+ description: Message,
+ instruction: Message,
+ diagram: Diagram,
+ },
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, Default)]
+pub struct Diagram {
+ pub nodes: Vec<DiagramNode>,
+ pub edges: Vec<DiagramEdge>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
+pub struct DiagramNode {
+ #[bincode(with_serde)]
+ pub position: Vec2,
+ pub label: Message,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
+pub struct DiagramEdge {
+ pub src: usize,
+ pub dst: usize,
+ pub label: Option<Message>,
+}
+
+impl Default for Book {
+ fn default() -> Self {
+ Book { pages: vec![] }
+ }
+}
diff --git a/server/protocol/src/helpers.rs b/server/protocol/src/helpers.rs
index b85c2f84..542f7754 100644
--- a/server/protocol/src/helpers.rs
+++ b/server/protocol/src/helpers.rs
@@ -1,10 +1,6 @@
+use crate::{Gamedata, Hand, ItemIndex, ItemLocation, PlayerID, Recipe, RecipeIndex, TileIndex};
use std::fmt::Display;
-use crate::{
- DocumentElement, Gamedata, Hand, ItemIndex, ItemLocation, PlayerID, Recipe, RecipeIndex,
- TileIndex,
-};
-
impl Gamedata {
pub fn tile_name(&self, index: TileIndex) -> &String {
&self.tile_names[index.0]
@@ -109,9 +105,3 @@ impl Display for Hand {
write!(f, "h{}", self.0)
}
}
-
-impl Default for DocumentElement {
- fn default() -> Self {
- Self::Document { es: vec![] }
- }
-}
diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs
index 3b87c7b8..146a5a92 100644
--- a/server/protocol/src/lib.rs
+++ b/server/protocol/src/lib.rs
@@ -25,6 +25,9 @@ use std::{collections::HashSet, sync::LazyLock};
pub use glam;
+use crate::book::Book;
+
+pub mod book;
pub mod helpers;
pub mod movement;
pub mod registry;
@@ -320,8 +323,9 @@ pub enum PacketC {
#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
#[serde(rename_all = "snake_case", tag = "menu", content = "data")]
pub enum Menu {
- Document(DocumentElement),
Score(Score),
+ Scoreboard(Scoreboard),
+ Book(Book),
AnnounceStart,
}
@@ -345,6 +349,17 @@ pub struct Score {
pub instant_recipes: usize,
}
+#[derive(Debug, Serialize, Deserialize, Encode, Decode, Clone, Default)]
+pub struct Scoreboard {
+ pub plays: usize,
+ pub best: Vec<ScoreboardEntry>,
+}
+#[derive(Debug, Serialize, Deserialize, Encode, Decode, Clone)]
+pub struct ScoreboardEntry {
+ pub players: Vec<String>,
+ pub score: Score,
+}
+
#[derive(Debug, Clone, Serialize, Encode, Decode, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Recipe {
@@ -377,73 +392,6 @@ pub enum ItemLocation {
Player(PlayerID, Hand),
}
-#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
-#[serde(rename_all = "snake_case", tag = "t")]
-pub enum DocumentElement {
- Document {
- es: Vec<DocumentElement>,
- },
- /// One page of the document, √2:1 aspect ratio
- Page {
- /// Name of background image
- background: Option<String>,
- es: Vec<DocumentElement>,
- },
- /// Implicit element layouting
- Container {
- es: Vec<DocumentElement>,
- },
- List {
- /// Should only contain par or text elements
- es: Vec<DocumentElement>,
- },
- /// Table with elements arranged as row arrays
- Table {
- es: Vec<Vec<DocumentElement>>,
- },
- /// A paragraph.
- Par {
- /// Should only contain text elements
- es: Vec<DocumentElement>,
- },
- /// A text span
- Text {
- s: Message,
- size: f32,
- color: Option<String>,
- font: Option<String>,
- #[serde(default)]
- bold: bool,
- },
- /// Document part that is only shown conditionally. Used for image attribution
- Conditional {
- cond: String,
- value: bool,
- e: Box<DocumentElement>,
- },
- /// Makes the child element clickable that jumps to the label with the same id
- Ref {
- id: String,
- e: Box<DocumentElement>,
- },
- /// Declares a label
- Label {
- id: String,
- e: Box<DocumentElement>,
- },
- Align {
- dir: DocumentAlign,
- e: Box<DocumentElement>,
- },
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
-#[serde(rename_all = "snake_case")]
-pub enum DocumentAlign {
- FlowEnd,
- Bottom,
-}
-
fn deser_i64<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
let x = f64::deserialize(deserializer)?;
Ok(x.trunc() as i64)
diff --git a/server/src/commands.rs b/server/src/commands.rs
index 460f6581..0e57a013 100644
--- a/server/src/commands.rs
+++ b/server/src/commands.rs
@@ -24,9 +24,7 @@ use crate::{
use anyhow::Result;
use clap::{Parser, ValueEnum};
use hurrycurry_bot::algos::ALGO_CONSTRUCTORS;
-use hurrycurry_protocol::{
- Character, DocumentElement, Menu, Message, PacketC, PlayerClass, PlayerID,
-};
+use hurrycurry_protocol::{Character, Menu, Message, PacketC, PlayerClass, PlayerID};
use std::{fmt::Write, time::Duration};
#[derive(Parser)]
@@ -110,7 +108,7 @@ enum DownloadType {
}
impl Server {
- pub async fn handle_command_parse(
+ pub fn handle_command_parse(
&mut self,
player: PlayerID,
command: &str,
@@ -126,12 +124,11 @@ impl Server {
)
.map_err(|e| TrError::Plain(e.to_string()))?,
&mut replies,
- )
- .await?;
+ )?;
}
Ok(replies)
}
- async fn handle_command(
+ fn handle_command(
&mut self,
player: PlayerID,
command: Command,
@@ -162,8 +159,7 @@ impl Server {
}
let data = self
.index
- .generate(&spec)
- .await
+ .generate_with_book(&spec)
.map_err(|e| TrError::Plain(e.to_string()))?;
self.load(data, timer.map(Duration::from_secs));
if !skip_announce {
@@ -190,8 +186,7 @@ impl Server {
.ok();
self.load(
self.index
- .generate("lobby")
- .await
+ .generate_with_book("lobby")
.map_err(|e| TrError::Plain(e.to_string()))?,
None,
);
@@ -202,8 +197,7 @@ impl Server {
}
self.load(
self.index
- .generate(&self.game.data.current_map)
- .await
+ .generate_with_book(&self.game.data.current_map)
.map_err(|e| TrError::Plain(e.to_string()))?,
None,
);
@@ -211,14 +205,13 @@ impl Server {
Command::ReloadIndex => {
self.index
.reload()
- .await
.map_err(|e| TrError::Plain(e.to_string()))?;
}
- Command::Book => replies.push(PacketC::Menu(Menu::Document(self.data.book.clone()))),
+ Command::Book => replies.push(PacketC::Menu(Menu::Book(self.data.book.clone()))),
Command::Download { r#type, name } => {
let source = match r#type {
- DownloadType::Map => self.index.read_map(&name).await,
- DownloadType::Recipes => self.index.read_recipes(&name).await,
+ DownloadType::Map => self.index.read_map(&name),
+ DownloadType::Recipes => self.index.read_recipes(&name),
}
.map_err(|e| TrError::Plain(e.to_string()))?;
replies.push(PacketC::ServerMessage {
@@ -288,45 +281,45 @@ impl Server {
error: false,
});
} else {
- replies.push(PacketC::Menu(Menu::Document(DocumentElement::Document {
- es: vec![DocumentElement::Page {
- es: vec![
- DocumentElement::Par {
- es: vec![DocumentElement::Text {
- s: Message::Translation {
- id: "c.menu.scoreboard".to_string(),
- params: vec![],
- },
- size: 30.,
- bold: false,
- color: None,
- font: None,
- }],
- },
- DocumentElement::List {
- es: board
- .best
- .iter()
- .take(10)
- .enumerate()
- .map(|(place, entry)| DocumentElement::Text {
- s: trm!(
- "c.menu.scoreboard.entry",
- s = (place + 1).to_string(),
- s = entry.score.points.to_string(),
- s = entry.players.clone().join(", ")
- ),
- size: 15.,
- bold: false,
- color: None,
- font: None,
- })
- .collect(),
- },
- ],
- background: None,
- }],
- })));
+ // replies.push(PacketC::Menu(Menu::Document(DocumentElement::Document {
+ // es: vec![DocumentElement::Page {
+ // es: vec![
+ // DocumentElement::Par {
+ // es: vec![DocumentElement::Text {
+ // s: Message::Translation {
+ // id: "c.menu.scoreboard".to_string(),
+ // params: vec![],
+ // },
+ // size: 30.,
+ // bold: false,
+ // color: None,
+ // font: None,
+ // }],
+ // },
+ // DocumentElement::List {
+ // es: board
+ // .best
+ // .iter()
+ // .take(10)
+ // .enumerate()
+ // .map(|(place, entry)| DocumentElement::Text {
+ // s: trm!(
+ // "c.menu.scoreboard.entry",
+ // s = (place + 1).to_string(),
+ // s = entry.score.points.to_string(),
+ // s = entry.players.clone().join(", ")
+ // ),
+ // size: 15.,
+ // bold: false,
+ // color: None,
+ // font: None,
+ // })
+ // .collect(),
+ // },
+ // ],
+ // background: None,
+ // }],
+ // })));
}
} else {
replies.push(PacketC::ServerMessage {
diff --git a/server/src/data/mod.rs b/server/src/data/mod.rs
index b56ea1a1..819d8dd1 100644
--- a/server/src/data/mod.rs
+++ b/server/src/data/mod.rs
@@ -23,19 +23,19 @@ use anyhow::{anyhow, bail, Context, Result};
use demands::generate_demands;
use hurrycurry_bot::algos::ALGO_CONSTRUCTORS;
use hurrycurry_protocol::{
+ book::Book,
glam::{IVec2, Vec2},
- DocumentElement, Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex,
+ Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex,
};
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
- fs::File,
+ fs::{read_to_string, File},
path::PathBuf,
str::FromStr,
sync::{Mutex, RwLock},
time::Duration,
};
-use tokio::fs::read_to_string;
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
@@ -96,7 +96,7 @@ pub struct Serverdata {
pub customer_spawn: Vec2,
pub score_baseline: i64,
pub default_timer: Option<Duration>,
- pub book: DocumentElement,
+ pub book: Book,
pub flags: ServerdataFlags,
}
@@ -122,53 +122,57 @@ fn data_dir() -> PathBuf {
}
impl DataIndex {
- pub async fn load() -> Result<Self> {
+ pub fn load() -> Result<Self> {
let mut s = Self::default();
- s.reload().await?;
+ s.reload()?;
Ok(s)
}
- pub async fn reload(&mut self) -> Result<()> {
+ pub fn reload(&mut self) -> Result<()> {
*self = serde_yml::from_reader(File::open(data_dir().join("index.yaml"))?)?;
Ok(())
}
- pub async fn read_map(&self, name: &str) -> Result<String> {
+ pub fn read_map(&self, name: &str) -> Result<String> {
// Scary!
if name.contains("..") || name.starts_with("/") || name.contains("//") {
bail!("illegal map path");
}
let path = data_dir().join(format!("maps/{name}.yaml"));
- Ok(read_to_string(path).await?)
+ Ok(read_to_string(path)?)
}
- pub async fn read_recipes(&self, name: &str) -> Result<String> {
+ pub fn read_recipes(&self, name: &str) -> Result<String> {
if !self.recipes.contains(name) {
bail!("unknown recipes: {name:?}");
}
let path = data_dir().join(format!("recipes/{name}.yaml"));
- Ok(read_to_string(path).await?)
+ Ok(read_to_string(path)?)
}
- pub async fn generate(&self, map: &str) -> Result<(Gamedata, Serverdata, Entities)> {
+ pub fn generate(&self, map: &str) -> Result<(Gamedata, Serverdata, Entities)> {
let map_in: MapDecl = serde_yml::from_str(
&self
.read_map(map)
- .await
.context(anyhow!("Failed to read map file ({map})"))?,
)
.context(anyhow!("Failed to parse map file ({map})"))?;
let recipes_in = serde_yml::from_str(
&self
.read_recipes(map_in.recipes.as_deref().unwrap_or("default"))
- .await
.context("Failed read recipe file")?,
)
.context("Failed to parse recipe file")?;
- let book = serde_json::from_str(
- &read_to_string(data_dir().join("book.json"))
- .await
- .context("Failed to read book file")?,
+
+ build_data(&self.maps, map.to_string(), map_in, recipes_in)
+ }
+ pub fn generate_with_book(&self, map: &str) -> Result<(Gamedata, Serverdata, Entities)> {
+ let (gd, mut sd, es) = self.generate(map)?;
+ sd.book = self.read_book()?;
+ Ok((gd, sd, es))
+ }
+ pub fn read_book(&self) -> Result<Book> {
+ serde_json::from_str(
+ &read_to_string(data_dir().join("book.json")).context("Failed to read book file")?,
)
- .context("Failed to parse book file")?;
- build_data(&self.maps, map.to_string(), map_in, recipes_in, book)
+ .context("Failed to parse book file")
}
}
@@ -177,7 +181,6 @@ pub fn build_data(
map_name: String,
map_in: MapDecl,
recipes_in: Vec<RecipeDecl>,
- book: DocumentElement,
) -> Result<(Gamedata, Serverdata, Entities)> {
let reg = ItemTileRegistry::default();
let mut recipes = Vec::new();
@@ -330,7 +333,7 @@ pub fn build_data(
flags: map_in.flags,
customer_spawn,
default_timer,
- book,
+ book: Book::default(),
score_baseline: map_in.score_baseline,
},
entities,
diff --git a/server/src/entity/book.rs b/server/src/entity/book.rs
index 54dc4d2c..566eb0a9 100644
--- a/server/src/entity/book.rs
+++ b/server/src/entity/book.rs
@@ -32,7 +32,7 @@ impl Entity for Book {
) -> Result<bool, TrError> {
if pos == Some(self.0) {
if let Some(r) = c.replies {
- r.push(PacketC::Menu(Menu::Document(c.serverdata.book.clone())));
+ r.push(PacketC::Menu(Menu::Book(c.serverdata.book.clone())));
}
return Ok(true);
}
diff --git a/server/src/main.rs b/server/src/main.rs
index 3cc6785a..ef397c7d 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -159,8 +159,8 @@ async fn run(args: Args) -> anyhow::Result<()> {
let (tx, rx) = broadcast::channel::<PacketC>(128 * 1024);
- let mut state = Server::new(tx).await?;
- state.load(state.index.generate("lobby").await?, None);
+ let mut state = Server::new(tx)?;
+ state.load(state.index.generate_with_book("lobby")?, None);
let state = Arc::new(RwLock::new(state));
#[cfg(feature = "register")]
@@ -197,7 +197,7 @@ async fn run(args: Args) -> anyhow::Result<()> {
let mut tick = interval(Duration::from_secs_f32(dt));
loop {
tick.tick().await;
- if let Err(e) = state.write().await.tick_outer(dt).await {
+ if let Err(e) = state.write().await.tick_outer(dt) {
warn!("Tick failed: {e}");
}
}
@@ -348,19 +348,26 @@ mod test {
#[test]
fn run() {
- harness(async { Server::new(broadcast::channel(1024).0).await.unwrap() });
+ harness(async { Server::new(broadcast::channel(1024).0).unwrap() });
}
#[test]
fn map_load() {
harness(async {
- let mut s = Server::new(broadcast::channel(1024).0).await.unwrap();
- s.load(s.index.generate("lobby").await.unwrap(), None);
+ let mut s = Server::new(broadcast::channel(1024).0).unwrap();
+ s.load(s.index.generate("5star").unwrap(), None);
+ });
+ }
+ #[test]
+ fn map_load_book() {
+ harness(async {
+ let mut s = Server::new(broadcast::channel(1024).0).unwrap();
+ s.load(s.index.generate_with_book("lobby").unwrap(), None);
});
}
#[test]
fn tick() {
harness(async {
- let mut s = Server::new(broadcast::channel(1024).0).await.unwrap();
+ let mut s = Server::new(broadcast::channel(1024).0).unwrap();
for _ in 0..100 {
s.tick(0.1);
}
@@ -369,7 +376,7 @@ mod test {
#[test]
fn packet_sender_verif() {
harness(async {
- let mut s = Server::new(broadcast::channel(1024).0).await.unwrap();
+ let mut s = Server::new(broadcast::channel(1024).0).unwrap();
for (conn, p) in [
PacketS::Effect {
diff --git a/server/src/scoreboard.rs b/server/src/scoreboard.rs
index 60b9356c..e97e22b2 100644
--- a/server/src/scoreboard.rs
+++ b/server/src/scoreboard.rs
@@ -17,36 +17,27 @@
*/
use anyhow::Result;
use directories::ProjectDirs;
-use hurrycurry_protocol::Score;
+use hurrycurry_protocol::{Score, Scoreboard, ScoreboardEntry};
use log::warn;
use serde::{Deserialize, Serialize};
-use std::{cmp::Reverse, collections::HashMap};
-use tokio::{
+use std::{
+ cmp::Reverse,
+ collections::HashMap,
fs::{create_dir_all, read_to_string, rename, File},
- io::AsyncWriteExt,
+ io::Write,
};
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ScoreboardStore {
maps: HashMap<String, Scoreboard>,
}
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
-pub struct Scoreboard {
- pub plays: usize,
- pub best: Vec<ScoreboardEntry>,
-}
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct ScoreboardEntry {
- pub players: Vec<String>,
- pub score: Score,
-}
fn project_dirs() -> Option<ProjectDirs> {
ProjectDirs::from("org", "hurrycurry", "hurrycurry")
}
impl ScoreboardStore {
- pub async fn load() -> Result<Self> {
+ pub fn load() -> Result<Self> {
let Some(dir) = project_dirs() else {
warn!("scoreboard load skipped; no data dir for this platform");
return Ok(Self::default());
@@ -54,24 +45,21 @@ impl ScoreboardStore {
// TOCTOU because its easier that way
let path = dir.data_dir().join("scoreboards.json");
if !path.exists() {
- create_dir_all(dir.data_dir()).await?;
- ScoreboardStore::default().save().await?;
+ create_dir_all(dir.data_dir())?;
+ ScoreboardStore::default().save()?;
}
- let s = read_to_string(path).await?;
+ let s = read_to_string(path)?;
Ok(serde_json::from_str(&s)?)
}
- pub async fn save(&self) -> Result<()> {
+ pub fn save(&self) -> Result<()> {
let Some(dir) = project_dirs() else {
warn!("scoreboard save skipped; no data dir for this platform");
return Ok(());
};
let path = dir.data_dir().join("scoreboards.json");
let buffer_path = dir.data_dir().join("scoreboards.json~");
- File::create(&buffer_path)
- .await?
- .write_all(serde_json::to_string(self)?.as_bytes())
- .await?;
- rename(buffer_path, path).await?;
+ File::create(&buffer_path)?.write_all(serde_json::to_string(self)?.as_bytes())?;
+ rename(buffer_path, path)?;
Ok(())
}
pub fn get<'a>(&'a self, map: &str) -> Option<&'a Scoreboard> {
diff --git a/server/src/server.rs b/server/src/server.rs
index 2fe0be17..f97ca69c 100644
--- a/server/src/server.rs
+++ b/server/src/server.rs
@@ -314,12 +314,10 @@ impl GameServerExt for Game {
}
impl Server {
- pub async fn new(tx: Sender<PacketC>) -> Result<Self> {
+ pub fn new(tx: Sender<PacketC>) -> Result<Self> {
Ok(Self {
game: Game::default(),
- index: DataIndex::load()
- .await
- .context("Failed to load data index")?,
+ index: DataIndex::load().context("Failed to load data index")?,
tx,
start_pause_timer: 0.,
packet_out: VecDeque::new(),
@@ -332,7 +330,6 @@ impl Server {
packet_loopback: VecDeque::new(),
last_movement_update: HashMap::default(),
scoreboard: ScoreboardStore::load()
- .await
.context("Failed to load scoreboards")?,
editor_address: None,
paused: false,
diff --git a/server/src/state.rs b/server/src/state.rs
index 5eb4059a..19c38fd2 100644
--- a/server/src/state.rs
+++ b/server/src/state.rs
@@ -21,7 +21,7 @@ use hurrycurry_protocol::{Message, PacketC, PacketS, PlayerID};
use log::{debug, info, trace};
impl Server {
- pub async fn tick_outer(&mut self, dt: f32) -> anyhow::Result<()> {
+ pub fn tick_outer(&mut self, dt: f32) -> anyhow::Result<()> {
if self.start_pause_timer > 0. {
self.start_pause_timer -= dt
}
@@ -46,8 +46,8 @@ impl Server {
if !self.paused {
let r = self.tick(dt);
if let Some((name, timer)) = r {
- self.scoreboard.save().await?;
- self.load(self.index.generate(&name).await?, timer);
+ self.scoreboard.save()?;
+ self.load(self.index.generate_with_book(&name)?, timer);
}
}
while let Some(p) = self.packet_out.pop_front() {
@@ -78,7 +78,7 @@ impl Server {
player,
..
} if let Some(command) = text.strip_prefix("/") => {
- match self.handle_command_parse(*player, command).await {
+ match self.handle_command_parse(*player, command) {
Ok(packets) => return Ok(packets),
Err(e) => {
return Ok(vec![PacketC::ServerMessage {
@@ -116,8 +116,7 @@ impl Server {
.ok();
self.load(
self.index
- .generate("lobby")
- .await
+ .generate_with_book("lobby")
.map_err(|m| tre!("s.error.map_load", s = format!("{m}")))?,
None,
);
diff --git a/server/tools/Cargo.toml b/server/tools/Cargo.toml
new file mode 100644
index 00000000..1a6d6aa3
--- /dev/null
+++ b/server/tools/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "hurrycurry-tools"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+anyhow = "1.0.100"
+log = "0.4.28"
+env_logger = "0.11.8"
+clap = { version = "4.5.47", features = ["derive"] }
+hurrycurry-protocol = { path = "../protocol" }
+hurrycurry-server = { path = ".." }
+serde_json = "1.0.145"
+serde = { version = "1.0.225", features = ["derive"] }
diff --git a/server/tools/src/book.rs b/server/tools/src/book.rs
new file mode 100644
index 00000000..6c871274
--- /dev/null
+++ b/server/tools/src/book.rs
@@ -0,0 +1,41 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright (C) 2025 Hurry Curry! Contributors
+
+ 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::{diagram_layout::diagram_layout, recipe_diagram::recipe_diagram};
+use anyhow::Result;
+use hurrycurry_protocol::{
+ ItemIndex, Message,
+ book::{Book, BookPage},
+};
+
+pub fn book() -> Result<()> {
+ let mut diagram = recipe_diagram(&["plate:cheese-leek-soup".to_owned()])?;
+ diagram_layout(&mut diagram)?;
+
+ let mut pages = Vec::new();
+
+ pages.push(BookPage::Recipe {
+ description: Message::Item(ItemIndex(0)),
+ instruction: Message::Item(ItemIndex(0)),
+ diagram,
+ });
+
+ let book = Book { pages };
+ println!("{}", serde_json::to_string_pretty(&book).unwrap());
+ Ok(())
+}
diff --git a/server/tools/src/diagram_layout.rs b/server/tools/src/diagram_layout.rs
new file mode 100644
index 00000000..e6ae2d76
--- /dev/null
+++ b/server/tools/src/diagram_layout.rs
@@ -0,0 +1,75 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright (C) 2025 Hurry Curry! Contributors
+
+ 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, anyhow};
+use hurrycurry_protocol::{book::Diagram, glam::Vec2};
+use serde::Deserialize;
+use std::{
+ collections::BTreeMap,
+ fmt::Write as W2,
+ io::Write,
+ process::{Command, Stdio},
+};
+
+pub struct Layout {
+ pub nodes: BTreeMap<String, Vec2>,
+ pub edges: Vec<(usize, usize)>,
+}
+
+pub fn diagram_layout(diagram: &mut Diagram) -> Result<()> {
+ let mut child = Command::new("dot")
+ .arg("-Tjson")
+ .arg("-Kdot")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()?;
+
+ let mut out = String::new();
+ writeln!(out, "digraph {{")?;
+ for edge in &diagram.edges {
+ writeln!(out, "k{} -> k{}", edge.src, edge.dst)?;
+ }
+ writeln!(out, "}}")?;
+
+ child.stdin.as_mut().unwrap().write_all(out.as_bytes())?;
+ let output = child.wait_with_output()?;
+
+ #[derive(Deserialize)]
+ struct Out {
+ objects: Vec<Object>,
+ }
+ #[derive(Deserialize)]
+ struct Object {
+ name: String,
+ pos: String,
+ }
+ let graph: Out = serde_json::from_slice(&output.stdout)?;
+ for o in graph.objects {
+ let pos = o.pos.split_once(",").ok_or(anyhow!("malformed position"))?;
+ let pos = Vec2::new(pos.0.parse()?, pos.1.parse()?);
+
+ let index = o
+ .name
+ .strip_prefix("k")
+ .ok_or(anyhow!("invalid node name"))?
+ .parse::<usize>()?;
+ diagram.nodes[index].position = pos
+ }
+
+ Ok(())
+}
diff --git a/server/src/bin/graph.rs b/server/tools/src/graph.rs
index 000be9e7..53f70d99 100644
--- a/server/src/bin/graph.rs
+++ b/server/tools/src/graph.rs
@@ -19,20 +19,19 @@ use anyhow::Result;
use hurrycurry_protocol::{Demand, ItemIndex, Recipe, RecipeIndex};
use hurrycurry_server::data::DataIndex;
-#[tokio::main]
-async fn main() -> Result<()> {
+pub(crate) fn graph() -> Result<()> {
let mut index = DataIndex::default();
- index.reload().await?;
+ index.reload()?;
println!("digraph {{");
- let (data, _, _) = index.generate("5star").await?;
+ let (data, _, _) = index.generate("5star")?;
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::Passive { .. } => ("Passive", "#b5c42b"),
Recipe::Active { .. } => ("Active", "#47c42b"),
Recipe::Instant { .. } => ("Instant", "#5452d8"),
};
diff --git a/server/src/bin/graph_summary.rs b/server/tools/src/graph_summary.rs
index d22361c0..be53e768 100644
--- a/server/src/bin/graph_summary.rs
+++ b/server/tools/src/graph_summary.rs
@@ -20,14 +20,13 @@ use hurrycurry_protocol::{ItemIndex, Recipe, TileIndex};
use hurrycurry_server::data::DataIndex;
use std::collections::HashSet;
-#[tokio::main]
-async fn main() -> Result<()> {
+pub(crate) fn graph_summary() -> Result<()> {
let mut index = DataIndex::default();
- index.reload().await?;
+ index.reload()?;
println!("digraph {{");
- let (data, sdata, _) = index.generate("5star").await?;
+ let (data, sdata, _) = index.generate("5star")?;
struct Node {
inputs: Vec<ItemIndex>,
diff --git a/server/tools/src/main.rs b/server/tools/src/main.rs
new file mode 100644
index 00000000..bb8fbde2
--- /dev/null
+++ b/server/tools/src/main.rs
@@ -0,0 +1,46 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright (C) 2025 Hurry Curry! Contributors
+
+ 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 book;
+pub mod diagram_layout;
+pub mod graph;
+pub mod graph_summary;
+pub mod recipe_diagram;
+
+use crate::{book::book, graph::graph, graph_summary::graph_summary};
+use anyhow::Result;
+use clap::Parser;
+
+#[derive(Parser)]
+enum Action {
+ Graph,
+ GraphSummary,
+ Book,
+}
+
+fn main() -> Result<()> {
+ let action = Action::parse();
+
+ match action {
+ Action::Graph => graph()?,
+ Action::GraphSummary => graph_summary()?,
+ Action::Book => book()?,
+ }
+
+ Ok(())
+}
diff --git a/server/tools/src/recipe_diagram.rs b/server/tools/src/recipe_diagram.rs
new file mode 100644
index 00000000..25f8040c
--- /dev/null
+++ b/server/tools/src/recipe_diagram.rs
@@ -0,0 +1,112 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright (C) 2025 Hurry Curry! Contributors
+
+ 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 hurrycurry_protocol::book::{Diagram, DiagramEdge, DiagramNode};
+use hurrycurry_protocol::glam::Vec2;
+use hurrycurry_protocol::{ItemIndex, Message, RecipeIndex};
+use hurrycurry_server::data::DataIndex;
+use std::collections::{BTreeMap, BTreeSet, HashSet};
+
+pub(crate) fn recipe_diagram(target_items: &[String]) -> Result<Diagram> {
+ let mut index = DataIndex::default();
+ index.reload()?;
+
+ let (data, serverdata, _) = index.generate("5star")?;
+
+ let ambient_items = serverdata
+ .initial_map
+ .iter()
+ .flat_map(|(_, (_, item))| item)
+ .copied()
+ .collect::<HashSet<_>>();
+
+ let mut need = BTreeSet::from_iter(
+ target_items
+ .iter()
+ .map(|name| data.get_item_by_name(name).unwrap()),
+ );
+ let mut have = BTreeSet::<ItemIndex>::new();
+ let mut recipes = BTreeSet::new();
+
+ #[derive(PartialEq, PartialOrd, Eq, Ord)]
+ struct GraphRecipe {
+ index: RecipeIndex,
+ inputs: Vec<ItemIndex>,
+ outputs: Vec<ItemIndex>,
+ }
+
+ while let Some(item) = need.pop_last() {
+ for (ri, r) in data.recipes() {
+ if r.outputs().contains(&item) {
+ let gr = GraphRecipe {
+ inputs: r
+ .inputs()
+ .iter()
+ .filter(|i| !ambient_items.contains(i))
+ .copied()
+ .collect(),
+ outputs: r
+ .outputs()
+ .iter()
+ .filter(|i| !ambient_items.contains(i))
+ .copied()
+ .collect(),
+ index: ri,
+ };
+ need.extend(&gr.inputs);
+ have.extend(&gr.outputs);
+ recipes.insert(gr);
+ }
+ }
+ }
+
+ let mut diag = Diagram::default();
+ let mut item_index = BTreeMap::new();
+ for i in have {
+ item_index.insert(i, diag.nodes.len());
+ diag.nodes.push(DiagramNode {
+ label: Message::Item(i),
+ position: Vec2::ZERO,
+ });
+ }
+ for r in recipes {
+ let index = diag.nodes.len();
+ diag.nodes.push(DiagramNode {
+ position: Vec2::ZERO,
+ label: Message::Text("blub".to_string()),
+ });
+
+ for i in r.inputs {
+ diag.edges.push(DiagramEdge {
+ src: item_index[&i],
+ dst: index,
+ label: None,
+ });
+ }
+ for o in r.outputs {
+ diag.edges.push(DiagramEdge {
+ src: index,
+ dst: item_index[&o],
+ label: None,
+ });
+ }
+ }
+
+ Ok(diag)
+}