From 3951a63d9393ed7e1381bbb9da95dfb2dd2d6b13 Mon Sep 17 00:00:00 2001
From: metamuffin <metamuffin@disroot.org>
Date: Mon, 14 Oct 2024 00:24:15 +0200
Subject: [PATCH] book intermediate representation and packets for sending to
 client

---
 book/book.js                   |   4 +-
 book/book.typ                  |   4 +-
 data/.gitignore                |   1 +
 data/makefile                  |   7 +-
 data/recipes/.gitignore        |   1 -
 server/protocol/src/helpers.rs | 110 ++++++++++++++++++++++++++++
 server/protocol/src/lib.rs     | 129 ++++++++-------------------------
 server/src/commands.rs         |   2 +-
 server/src/data/mod.rs         |  19 +++--
 server/src/entity/book.rs      |   2 +-
 server/src/entity/customers.rs |   1 +
 server/src/entity/mod.rs       |   7 +-
 server/src/server.rs           |   4 +
 13 files changed, 177 insertions(+), 114 deletions(-)
 create mode 100644 data/.gitignore
 create mode 100644 server/protocol/src/helpers.rs

diff --git a/book/book.js b/book/book.js
index 84c0e9d..094068c 100644
--- a/book/book.js
+++ b/book/book.js
@@ -1,5 +1,5 @@
 
-const tr = t => ({ translated: t })
+const tr = t => ({ translation: { id: t, params: [] } })
 const plain = t => ({ text: t })
 const par_raw = (s, th, bold) => ({
     t: "par", es: [{
@@ -42,7 +42,7 @@ const about = () => ({
         par("b.about.name"),
         { t: "text", s: tr("b.about.image_attrib"), color: "#000000", size: 20, font: "FreeSans" },
         {
-            t: "table", cols: 2, es: [
+            t: "table", es: [
                 [par("b.about.image_attrib.name", false, true), par("b.about.image_attrib.author", false, true)],
                 [par("b.nigiri"), par_raw(plain("Ahtziri Lagarde(unsplash)"))],
                 [par("b.icecream"), par_raw(plain("Markus Spiske(unsplash), adapted"))],
diff --git a/book/book.typ b/book/book.typ
index 39f3cc4..14e617a 100644
--- a/book/book.typ
+++ b/book/book.typ
@@ -21,7 +21,7 @@
 }
 #let tr(s) = translations.at(s, default: text(fill: red)[TRANSLATION: #s])
 
-#let msgstr(o) = if "translated" in o { tr(o.translated) } else { o.text }
+#let msgstr(o) = if "translation" in o { tr(o.translation.id) } else { o.text }
 
 #let element(elem) = if elem.t == "document" [
     #for e in elem.es [
@@ -50,7 +50,7 @@
   ] else if elem.t == "par" [
     #par(..elem.es.map(element))
   ] else if elem.t == "table" [
-    #table(columns: elem.cols, ..elem.es.flatten().map(element))
+    #table(columns: elem.es.at(0).len(), ..elem.es.flatten().map(element))
   ] else [
     #elem
   ]
diff --git a/data/.gitignore b/data/.gitignore
new file mode 100644
index 0000000..08711f0
--- /dev/null
+++ b/data/.gitignore
@@ -0,0 +1 @@
+/book.json
diff --git a/data/makefile b/data/makefile
index 4c948a8..a504180 100644
--- a/data/makefile
+++ b/data/makefile
@@ -14,11 +14,14 @@
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 #
 SETS = default none anticurry
-all: $(patsubst %,recipes/%.yaml,$(SETS))
+ALL = $(patsubst %,recipes/%.yaml,$(SETS)) book.json
+all: $(ALL)
 graphs: recipes/default.svg
 
 JSR = deno run
 
+book.json: ../book/book.json
+	cp $< $@
 recipes/none.yaml:
 	echo > $@
 recipes/anticurry.yaml: recipes/default.yaml
@@ -33,4 +36,4 @@ recipes/%.svg: recipes/%.gv.txt
 	dot -Tsvg -Kdot >$@<$<
 
 clean:
-	$(RM) -r recipes/*.{yaml,gv.txt,svg}
+	$(RM) $(ALL)
diff --git a/data/recipes/.gitignore b/data/recipes/.gitignore
index a9e52ca..77c3640 100644
--- a/data/recipes/.gitignore
+++ b/data/recipes/.gitignore
@@ -1,4 +1,3 @@
 *.yaml
-!newRecipes.yaml
 *.gv.txt
 *.svg
diff --git a/server/protocol/src/helpers.rs b/server/protocol/src/helpers.rs
new file mode 100644
index 0000000..924d088
--- /dev/null
+++ b/server/protocol/src/helpers.rs
@@ -0,0 +1,110 @@
+use std::fmt::Display;
+
+use crate::{
+    DocumentElement, Gamedata, ItemIndex, ItemLocation, PlayerID, Recipe, RecipeIndex, TileIndex,
+};
+
+impl Gamedata {
+    pub fn tile_name(&self, index: TileIndex) -> &String {
+        &self.tile_names[index.0]
+    }
+    pub fn is_tile_colliding(&self, index: TileIndex) -> bool {
+        self.tile_collide[index.0]
+    }
+    pub fn is_tile_interactable(&self, index: TileIndex) -> bool {
+        self.tile_interact[index.0]
+    }
+    pub fn item_name(&self, index: ItemIndex) -> &String {
+        &self.item_names[index.0]
+    }
+    pub fn recipe(&self, index: RecipeIndex) -> &Recipe {
+        &self.recipes[index.0]
+    }
+    pub fn get_tile_by_name(&self, name: &str) -> Option<TileIndex> {
+        self.tile_names
+            .iter()
+            .position(|t| t == name)
+            .map(TileIndex)
+    }
+    pub fn get_item_by_name(&self, name: &str) -> Option<ItemIndex> {
+        self.item_names
+            .iter()
+            .position(|t| t == name)
+            .map(ItemIndex)
+    }
+    pub fn recipes(&self) -> impl Iterator<Item = (RecipeIndex, &Recipe)> {
+        self.recipes
+            .iter()
+            .enumerate()
+            .map(|(i, e)| (RecipeIndex(i), e))
+    }
+}
+
+impl Recipe {
+    pub fn tile(&self) -> Option<TileIndex> {
+        match self {
+            Recipe::Passive { tile, .. } => *tile,
+            Recipe::Active { tile, .. } => *tile,
+            Recipe::Instant { tile, .. } => *tile,
+        }
+    }
+    pub fn speed(&self) -> Option<f32> {
+        match self {
+            Recipe::Passive { speed, .. } => Some(*speed),
+            Recipe::Active { speed, .. } => Some(*speed),
+            _ => None,
+        }
+    }
+    pub fn revert_speed(&self) -> Option<f32> {
+        match self {
+            Recipe::Passive { revert_speed, .. } => *revert_speed,
+            _ => None,
+        }
+    }
+    pub fn warn(&self) -> bool {
+        match self {
+            Recipe::Passive { warn, .. } => *warn,
+            _ => false,
+        }
+    }
+    pub fn inputs(&self) -> Vec<ItemIndex> {
+        match self {
+            Recipe::Passive { input, .. } => vec![*input],
+            Recipe::Active { input, .. } => vec![*input],
+            Recipe::Instant { inputs, .. } => inputs.iter().flat_map(|e| e.to_owned()).collect(),
+        }
+    }
+    pub fn outputs(&self) -> Vec<ItemIndex> {
+        match self {
+            Recipe::Passive { output, .. } => output.iter().copied().collect(),
+            Recipe::Active { outputs, .. } => outputs.iter().flat_map(|e| e.to_owned()).collect(),
+            Recipe::Instant { outputs, .. } => outputs.iter().flat_map(|e| e.to_owned()).collect(),
+        }
+    }
+    pub fn supports_tile(&self, tile: Option<TileIndex>) -> bool {
+        if let Some(tile_constraint) = self.tile() {
+            if let Some(tile) = tile {
+                tile == tile_constraint
+            } else {
+                false
+            }
+        } else {
+            true
+        }
+    }
+}
+
+impl Display for ItemLocation {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ItemLocation::Tile(pos) => write!(f, "tile({pos})"),
+            ItemLocation::Player(PlayerID(id)) => write!(f, "player({id})"),
+        }
+    }
+}
+
+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 1acf799..3337db0 100644
--- a/server/protocol/src/lib.rs
+++ b/server/protocol/src/lib.rs
@@ -21,10 +21,11 @@ use bincode::{
 };
 use glam::{IVec2, Vec2};
 use serde::{Deserialize, Serialize};
-use std::{collections::HashSet, fmt::Display, sync::LazyLock};
+use std::{collections::HashSet, sync::LazyLock};
 
 pub use glam;
 
+pub mod helpers;
 pub mod movement;
 pub mod registry;
 
@@ -290,7 +291,7 @@ pub enum PacketC {
 #[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
 #[serde(rename_all = "snake_case", tag = "menu", content = "data")]
 pub enum Menu {
-    Book,
+    Document(DocumentElement),
     Score(Score),
 }
 
@@ -339,96 +340,6 @@ pub enum Recipe {
     },
 }
 
-impl Gamedata {
-    pub fn tile_name(&self, index: TileIndex) -> &String {
-        &self.tile_names[index.0]
-    }
-    pub fn is_tile_colliding(&self, index: TileIndex) -> bool {
-        self.tile_collide[index.0]
-    }
-    pub fn is_tile_interactable(&self, index: TileIndex) -> bool {
-        self.tile_interact[index.0]
-    }
-    pub fn item_name(&self, index: ItemIndex) -> &String {
-        &self.item_names[index.0]
-    }
-    pub fn recipe(&self, index: RecipeIndex) -> &Recipe {
-        &self.recipes[index.0]
-    }
-    pub fn get_tile_by_name(&self, name: &str) -> Option<TileIndex> {
-        self.tile_names
-            .iter()
-            .position(|t| t == name)
-            .map(TileIndex)
-    }
-    pub fn get_item_by_name(&self, name: &str) -> Option<ItemIndex> {
-        self.item_names
-            .iter()
-            .position(|t| t == name)
-            .map(ItemIndex)
-    }
-    pub fn recipes(&self) -> impl Iterator<Item = (RecipeIndex, &Recipe)> {
-        self.recipes
-            .iter()
-            .enumerate()
-            .map(|(i, e)| (RecipeIndex(i), e))
-    }
-}
-
-impl Recipe {
-    pub fn tile(&self) -> Option<TileIndex> {
-        match self {
-            Recipe::Passive { tile, .. } => *tile,
-            Recipe::Active { tile, .. } => *tile,
-            Recipe::Instant { tile, .. } => *tile,
-        }
-    }
-    pub fn speed(&self) -> Option<f32> {
-        match self {
-            Recipe::Passive { speed, .. } => Some(*speed),
-            Recipe::Active { speed, .. } => Some(*speed),
-            _ => None,
-        }
-    }
-    pub fn revert_speed(&self) -> Option<f32> {
-        match self {
-            Recipe::Passive { revert_speed, .. } => *revert_speed,
-            _ => None,
-        }
-    }
-    pub fn warn(&self) -> bool {
-        match self {
-            Recipe::Passive { warn, .. } => *warn,
-            _ => false,
-        }
-    }
-    pub fn inputs(&self) -> Vec<ItemIndex> {
-        match self {
-            Recipe::Passive { input, .. } => vec![*input],
-            Recipe::Active { input, .. } => vec![*input],
-            Recipe::Instant { inputs, .. } => inputs.iter().flat_map(|e| e.to_owned()).collect(),
-        }
-    }
-    pub fn outputs(&self) -> Vec<ItemIndex> {
-        match self {
-            Recipe::Passive { output, .. } => output.iter().copied().collect(),
-            Recipe::Active { outputs, .. } => outputs.iter().flat_map(|e| e.to_owned()).collect(),
-            Recipe::Instant { outputs, .. } => outputs.iter().flat_map(|e| e.to_owned()).collect(),
-        }
-    }
-    pub fn supports_tile(&self, tile: Option<TileIndex>) -> bool {
-        if let Some(tile_constraint) = self.tile() {
-            if let Some(tile) = tile {
-                tile == tile_constraint
-            } else {
-                false
-            }
-        } else {
-            true
-        }
-    }
-}
-
 #[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, Copy, PartialEq, Eq, Hash)]
 #[serde(rename_all = "snake_case")]
 pub enum ItemLocation {
@@ -436,11 +347,31 @@ pub enum ItemLocation {
     Player(PlayerID),
 }
 
-impl Display for ItemLocation {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            ItemLocation::Tile(pos) => write!(f, "tile({pos})"),
-            ItemLocation::Player(PlayerID(id)) => write!(f, "player({id})"),
-        }
-    }
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
+#[serde(rename_all = "snake_case", tag = "t")]
+pub enum DocumentElement {
+    Document {
+        es: Vec<DocumentElement>,
+    },
+    Page {
+        background: Option<String>,
+        es: Vec<DocumentElement>,
+    },
+    List {
+        es: Vec<DocumentElement>,
+    },
+    Table {
+        es: Vec<Vec<DocumentElement>>,
+    },
+    Par {
+        es: Vec<DocumentElement>,
+    },
+    Text {
+        s: Message,
+        color: String,
+        size: f32,
+        font: String,
+        #[serde(default)]
+        bold: bool,
+    },
 }
diff --git a/server/src/commands.rs b/server/src/commands.rs
index feb6ab6..25b51bf 100644
--- a/server/src/commands.rs
+++ b/server/src/commands.rs
@@ -195,7 +195,7 @@ impl Server {
                     .await
                     .map_err(|e| TrError::Plain(e.to_string()))?;
             }
-            Command::Book => replies.push(PacketC::Menu(Menu::Book)),
+            Command::Book => replies.push(PacketC::Menu(Menu::Document(self.data.book.clone()))),
             Command::Download { r#type, name } => {
                 let source = match r#type {
                     DownloadType::Map => self.index.read_map(&name).await,
diff --git a/server/src/data/mod.rs b/server/src/data/mod.rs
index 39684c6..549574c 100644
--- a/server/src/data/mod.rs
+++ b/server/src/data/mod.rs
@@ -20,12 +20,12 @@ pub mod demands;
 pub mod index;
 
 use crate::entity::{construct_entity, Entities, EntityDecl};
-use anyhow::{anyhow, bail, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use demands::generate_demands;
 use hurrycurry_bot::algos::ALGO_CONSTRUCTORS;
 use hurrycurry_protocol::{
     glam::{IVec2, Vec2},
-    Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex,
+    DocumentElement, Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex,
 };
 use serde::{Deserialize, Serialize};
 use std::{
@@ -87,14 +87,15 @@ pub struct DemandDecl {
     points: i64,
 }
 
-#[derive(Debug,Clone, Default)]
+#[derive(Debug, Clone, Default)]
 #[rustfmt::skip]
 pub struct Serverdata {
     pub initial_map: HashMap<IVec2, (TileIndex, Option<ItemIndex>)>,
     pub chef_spawn: Vec2,
     pub customer_spawn: Vec2,
     pub score_baseline: i64,
-    pub default_timer: Option<Duration>
+    pub default_timer: Option<Duration>,
+    pub book: DocumentElement
 }
 
 #[derive(Debug, Deserialize, Default)]
@@ -145,7 +146,13 @@ impl DataIndex {
                 .read_recipes(map_in.recipes.as_deref().unwrap_or("default"))
                 .await?,
         )?;
-        build_data(&self.maps, map.to_string(), map_in, recipes_in)
+        let book = serde_json::from_str(
+            &read_to_string(data_dir().join("book.json"))
+                .await
+                .context("loading book")?,
+        )
+        .context("invalid book")?;
+        build_data(&self.maps, map.to_string(), map_in, recipes_in, book)
     }
 }
 
@@ -154,6 +161,7 @@ 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();
@@ -304,6 +312,7 @@ pub fn build_data(
             chef_spawn,
             customer_spawn,
             default_timer,
+            book,
             score_baseline: map_in.score_baseline,
         },
         entities,
diff --git a/server/src/entity/book.rs b/server/src/entity/book.rs
index 7019776..8b152bb 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::Book));
+                r.push(PacketC::Menu(Menu::Document(c.serverdata.book.clone())));
             }
             return Ok(true);
         }
diff --git a/server/src/entity/customers.rs b/server/src/entity/customers.rs
index 1e8b6b1..0c3ced8 100644
--- a/server/src/entity/customers.rs
+++ b/server/src/entity/customers.rs
@@ -65,6 +65,7 @@ impl Entity for Customers {
         for bot in &mut self.customers {
             bot.tick(EntityContext {
                 game: c.game,
+                serverdata: c.serverdata,
                 packet_out: c.packet_out,
                 packet_in: c.packet_in,
                 score_changed: c.score_changed,
diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs
index 87656f0..044fcca 100644
--- a/server/src/entity/mod.rs
+++ b/server/src/entity/mod.rs
@@ -25,7 +25,11 @@ pub mod item_portal;
 pub mod player_portal;
 pub mod tutorial;
 
-use crate::{data::ItemTileRegistry, message::TrError, scoreboard::ScoreboardStore};
+use crate::{
+    data::{ItemTileRegistry, Serverdata},
+    message::TrError,
+    scoreboard::ScoreboardStore,
+};
 use anyhow::{anyhow, Result};
 use book::Book;
 use campaign::{Gate, GateCondition, Map};
@@ -47,6 +51,7 @@ pub type Entities = Vec<DynEntity>;
 
 pub struct EntityContext<'a> {
     pub game: &'a mut Game,
+    pub serverdata: &'a Serverdata,
     pub packet_out: &'a mut VecDeque<PacketC>,
     pub packet_in: &'a mut VecDeque<PacketS>,
     pub score_changed: &'a mut bool,
diff --git a/server/src/server.rs b/server/src/server.rs
index 18f78fc..d3e9ab5 100644
--- a/server/src/server.rs
+++ b/server/src/server.rs
@@ -340,6 +340,7 @@ impl Server {
                 packet_in: &mut self.packet_loopback,
                 score_changed: &mut self.score_changed,
                 scoreboard: &self.scoreboard,
+                serverdata: self.data.as_ref(),
                 replies: None,
                 dt: 0.,
                 load_map: &mut None,
@@ -447,6 +448,7 @@ impl Server {
                             packet_in: &mut self.packet_loopback,
                             score_changed: &mut self.score_changed,
                             load_map: &mut None,
+                            serverdata: &self.data,
                             scoreboard: &self.scoreboard,
                             replies: Some(replies),
                             dt: 0.,
@@ -699,6 +701,7 @@ impl Server {
                 score_changed: &mut self.score_changed,
                 packet_in: &mut self.packet_loopback,
                 scoreboard: &self.scoreboard,
+                serverdata: &self.data,
                 replies: None,
                 dt,
             }) {
@@ -714,6 +717,7 @@ impl Server {
                     score_changed: &mut self.score_changed,
                     packet_in: &mut self.packet_loopback,
                     scoreboard: &self.scoreboard,
+                    serverdata: &self.data,
                     replies: None,
                     dt: 0.,
                 });
-- 
2.47.0

