From 30bfe4dc801fdfd3ab8d7a5e36bba67eee70d33b Mon Sep 17 00:00:00 2001 From: metamuffin Date: Tue, 7 Oct 2025 17:32:43 +0200 Subject: Split book exporting to own crate; move locale tool to server dir --- server/book-export/Cargo.toml | 15 ++ server/book-export/src/book_html.css | 41 +++++ server/book-export/src/book_html.rs | 89 ++++++++++ server/book-export/src/diagram_svg.rs | 152 +++++++++++++++++ server/book-export/src/main.rs | 60 +++++++ server/data/src/book/mod.rs | 6 - server/locale-export/Cargo.toml | 9 + server/locale-export/src/main.rs | 313 ++++++++++++++++++++++++++++++++++ server/tools/Cargo.toml | 3 - server/tools/src/book_html.css | 41 ----- server/tools/src/book_html.rs | 89 ---------- server/tools/src/diagram_dot.rs | 11 +- server/tools/src/diagram_svg.rs | 152 ----------------- server/tools/src/main.rs | 33 +--- 14 files changed, 691 insertions(+), 323 deletions(-) create mode 100644 server/book-export/Cargo.toml create mode 100644 server/book-export/src/book_html.css create mode 100644 server/book-export/src/book_html.rs create mode 100644 server/book-export/src/diagram_svg.rs create mode 100644 server/book-export/src/main.rs create mode 100644 server/locale-export/Cargo.toml create mode 100644 server/locale-export/src/main.rs delete mode 100644 server/tools/src/book_html.css delete mode 100644 server/tools/src/book_html.rs delete mode 100644 server/tools/src/diagram_svg.rs (limited to 'server') diff --git a/server/book-export/Cargo.toml b/server/book-export/Cargo.toml new file mode 100644 index 00000000..d106dab8 --- /dev/null +++ b/server/book-export/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "hurrycurry-book-export" +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-locale = { path = "../locale" } +hurrycurry-data = { path = "../data" } +serde_json = "1.0.145" +markup = "0.15.0" diff --git a/server/book-export/src/book_html.css b/server/book-export/src/book_html.css new file mode 100644 index 00000000..9171aa5e --- /dev/null +++ b/server/book-export/src/book_html.css @@ -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 . + +*/ +body { + margin: 0px; + background-color: rgb(74, 74, 74); + scroll-behavior: smooth; +} + +.pagegroup { + margin-left: auto; + margin-right: auto; + display: block; + width: 64em; +} + +.page { + display: inline-block; + background-color: antiquewhite; + box-sizing: border-box; + padding: 2em; + margin: 1em; + aspect-ratio: 0.707; + width: 30em; + height: auto; + overflow-y: auto; +} diff --git a/server/book-export/src/book_html.rs b/server/book-export/src/book_html.rs new file mode 100644 index 00000000..08a12e88 --- /dev/null +++ b/server/book-export/src/book_html.rs @@ -0,0 +1,89 @@ +/* + 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 . + +*/ + +use crate::diagram_svg::diagram_svg; +use hurrycurry_locale::{ + Locale, + message::{MessageDisplayExt, PLAIN}, +}; +use hurrycurry_protocol::{ + Gamedata, Message, + book::{Book, BookPage, Diagram}, +}; + +pub fn render_html_book(data: &Gamedata, book: &Book, locale: &Locale) -> String { + BookR { book, data, locale }.to_string() +} + +markup::define! { + BookR<'a>(data: &'a Gamedata, book: &'a Book, locale: &'a Locale) { + @markup::doctype() + html { + head { + style { @include_str!("book_html.css").replace(" ", "").replace("\n", "") } + title { "Recipe Book - Hurry Curry!" } + } + body { + @for (index, page) in book.pages.iter().enumerate() { + @PageR { data, locale, page, index } + } + } + } + } + + PageR<'a>(index: usize, data: &'a Gamedata, locale: &'a Locale, page: &'a BookPage) { + section.pagegroup[id=format!("page{index}")] { + @match page { + BookPage::Cover => { + div.page {} + div.page {} + } + BookPage::Recipe { title, description, diagram } => { + div.page { + h1 { @MessageR { data, locale, message: title } } + p { @MessageR { data, locale, message: description } } + } + div.page { + @DiagramR { data, diagram } + } + } + BookPage::Text { .. } => { + div.page {} + div.page {} + } + BookPage::Contents { title, table } => { + div.page { + h1 { @MessageR { data, locale, message: title } } + ol { @for (label, page) in table { + li { a[href=format!("#page{page}")] { @MessageR { data, locale, message: label } } } + }} + } + div.page {} + } + } + } + } + + MessageR<'a>(data: &'a Gamedata, locale: &'a Locale, message: &'a Message) { + @message.display_message(*locale, *data, &PLAIN) + } + + DiagramR<'a>(data: &'a Gamedata, diagram: &'a Diagram) { + @markup::raw(diagram_svg(data, diagram).unwrap()) + } +} diff --git a/server/book-export/src/diagram_svg.rs b/server/book-export/src/diagram_svg.rs new file mode 100644 index 00000000..d006ce78 --- /dev/null +++ b/server/book-export/src/diagram_svg.rs @@ -0,0 +1,152 @@ +/* + 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 . + +*/ + +use anyhow::Result; +use hurrycurry_protocol::{ + Gamedata, Message, + book::{Diagram, DiagramNode, NodeStyle}, + glam::Vec2, +}; +use std::fmt::Write; + +const SIZE: f32 = 64.; +const HSIZE: f32 = SIZE / 2.; + +pub fn diagram_svg(data: &Gamedata, diagram: &Diagram) -> Result { + let mut out = String::new(); + + let (vb_min, vb_max) = diagram + .nodes + .iter() + .map(|n| (n.position, n.position)) + .reduce(|(xa, xb), (ya, yb)| (xa.min(ya), xb.max(yb))) + .unwrap(); + + writeln!( + out, + r#""#, + vb_min.x - HSIZE, + vb_min.y - HSIZE, + vb_max.x + HSIZE, + vb_max.y + HSIZE, + vb_max.x - vb_min.x + SIZE, + vb_max.y - vb_min.y + SIZE, + )?; + + for node in &diagram.nodes { + match node.label { + Message::Translation { .. } => {} + Message::Text(_) => {} + Message::Item(item_index) => { + image_node( + &mut out, + node, + format!("items/{}.png", data.item_name(item_index)), + )?; + } + Message::Tile(tile_index) => { + image_node( + &mut out, + node, + format!("tiles/{}.png", data.tile_name(tile_index)), + )?; + } + } + } + for edge in &diagram.edges { + let src_node = &diagram.nodes[edge.src]; + let dst_node = &diagram.nodes[edge.dst]; + let src = node_edge_connect_pos(src_node, dst_node); + let mut dst = node_edge_connect_pos(dst_node, src_node); + + let dir = (src - dst).normalize_or_zero(); + let tip0 = dst; + let tip1 = dst + (dir + dir.perp() * 0.5) * 10.; + let tip2 = dst + (dir + dir.perp() * -0.5) * 10.; + dst += dir * 5.; // prevent miter line cap from peeking out + + // line path + writeln!( + out, + r#""#, + src.x, src.y, dst.x, dst.y, + )?; + // tip path + writeln!( + out, + r#""#, + tip0.x, tip0.y, tip1.x, tip1.y, tip2.x, tip2.y + )?; + } + + writeln!(out, "")?; + Ok(out) +} + +fn node_edge_connect_pos(src: &DiagramNode, dst: &DiagramNode) -> Vec2 { + let dir = (dst.position - src.position).normalize_or_zero(); + if matches!( + src.style, + NodeStyle::FinalProduct | NodeStyle::IntermediateProduct + ) { + src.position + dir * HSIZE // circle + } else { + src.position + dir / dir.y.abs() * HSIZE // square (only +Y and -Y sides) + } +} + +pub(crate) fn node_color(node: &DiagramNode) -> &'static str { + match node.style { + NodeStyle::FinalProduct => "#555", + NodeStyle::IntermediateProduct => "#333", + NodeStyle::ProcessActive => "#47c42b", + NodeStyle::ProcessPassive => "#c4a32b", + NodeStyle::ProcessInstant => "#5452d8", + } +} + +fn image_node(out: &mut String, node: &DiagramNode, path: String) -> Result<()> { + if matches!( + node.style, + NodeStyle::FinalProduct | NodeStyle::IntermediateProduct + ) { + writeln!( + out, + r#""#, + node.position.x, + node.position.y, + HSIZE, + node_color(node) + )?; + } else { + writeln!( + out, + r#""#, + node.position.x - HSIZE, + node.position.y - HSIZE, + node_color(node) + )?; + } + writeln!( + out, + r#""#, + node.position.x - HSIZE, + node.position.y - HSIZE, + )?; + Ok(()) +} diff --git a/server/book-export/src/main.rs b/server/book-export/src/main.rs new file mode 100644 index 00000000..b57aa04f --- /dev/null +++ b/server/book-export/src/main.rs @@ -0,0 +1,60 @@ +/* + 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 . + +*/ +pub mod book_html; +pub mod diagram_svg; + +use crate::book_html::render_html_book; +use anyhow::Result; +use clap::{Parser, ValueEnum}; +use hurrycurry_data::{book::book, index::DataIndex}; +use hurrycurry_locale::FALLBACK_LOCALE; + +#[derive(Parser)] +struct Args { + #[arg(short, long)] + format: ExportFormat, + #[arg(short, long, default_value = "5star")] + map: String, +} + +#[derive(ValueEnum, Clone, Copy)] +enum ExportFormat { + Json, + Html, +} + +fn main() -> Result<()> { + env_logger::init_from_env("LOG"); + let args = Args::parse(); + + let mut index = DataIndex::default(); + index.reload()?; + let (data, serverdata) = index.generate(&args.map)?; + + let book = book(&data, &serverdata)?; + + match args.format { + ExportFormat::Json => { + println!("{}", serde_json::to_string(&book).unwrap()) + } + ExportFormat::Html => { + println!("{}", render_html_book(&data, &book, &FALLBACK_LOCALE)); + } + } + Ok(()) +} diff --git a/server/data/src/book/mod.rs b/server/data/src/book/mod.rs index f9b54d0d..263e8111 100644 --- a/server/data/src/book/mod.rs +++ b/server/data/src/book/mod.rs @@ -63,9 +63,3 @@ pub fn book(data: &Gamedata, serverdata: &Serverdata) -> Result { } Ok(Book { pages }) } - -pub fn print_book(data: &Gamedata, serverdata: &Serverdata) -> Result<()> { - let book = book(data, serverdata)?; - println!("{}", serde_json::to_string_pretty(&book).unwrap()); - Ok(()) -} diff --git a/server/locale-export/Cargo.toml b/server/locale-export/Cargo.toml new file mode 100644 index 00000000..89543937 --- /dev/null +++ b/server/locale-export/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "localetool" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.47", features = ["derive"] } +anyhow = "1.0.99" +serde_json = "1.0.145" diff --git a/server/locale-export/src/main.rs b/server/locale-export/src/main.rs new file mode 100644 index 00000000..23823716 --- /dev/null +++ b/server/locale-export/src/main.rs @@ -0,0 +1,313 @@ +use anyhow::{anyhow, Context, Result}; +use clap::Parser; +use std::{ + collections::BTreeMap, + fmt::Write as W2, + fs::{read_to_string, File}, + io::Write, + path::{Path, PathBuf}, +}; + +#[derive(Parser)] +enum Args { + ImportOldPot { + input: PathBuf, + output: PathBuf, + }, + ImportOldPo { + reference: PathBuf, + input: PathBuf, + output: PathBuf, + }, + ExportJson { + output: PathBuf, + inputs: Vec, + }, + ExportGodotCsv { + input_dir: PathBuf, + output: PathBuf, + }, + ExportPo { + #[arg(long)] + remap_ids: Option, + output: PathBuf, + inputs: Vec, + }, +} + +static NATIVE_LANGUAGE_NAMES: &[(&str, &str)] = &[ + ("en", "English"), + ("de", "Deutsch"), + ("fr", "Français"), + ("nl", "Nederlands"), + ("es", "Español"), + ("eu", "Euskara"), + ("ja", "日本語"), + ("he", "עִברִית"), + ("tr", "Türkçe"), + ("fi", "Suomi"), + ("ar", "العربية"), + ("zh_Hans", "中文 (简化字)"), + ("zh_Hant", "中文 (繁體字)"), + ("pl", "Polski"), + ("pt", "Português"), + ("it", "Italiano"), + ("ko", "한국인"), + ("el", "ελληνικά"), + ("ru", "русский"), +]; + +fn export_load(inputs: &[PathBuf]) -> Result> { + let mut ini = BTreeMap::new(); + for path in inputs { + let f = load_ini(path)?; + for (k, v) in f { + ini.entry(k).or_insert(v); + } + } + for &(code, name) in NATIVE_LANGUAGE_NAMES { + ini.insert(format!("c.settings.ui.language.{code}"), name.to_owned()); + } + Ok(ini) +} + +fn main() -> Result<()> { + let args = Args::parse(); + match args { + Args::ExportJson { inputs, output } => { + let ini = export_load(&inputs)?; + File::create(output)?.write_all(serde_json::to_string(&ini)?.as_bytes())?; + Ok(()) + } + Args::ExportPo { + remap_ids: id_map, + output, + inputs, + } => { + let ini = export_load(&inputs)?; + let id_map = id_map.map(|path| load_ini(&path)).transpose()?; + File::create(output)?.write_all( + format!( + r#" +msgid "" +msgstr "" +"Language: {}\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +{}"#, + inputs + .first() + .ok_or(anyhow!("at least one input required"))? + .file_stem() + .ok_or(anyhow!("file name empty"))? + .to_string_lossy(), + ini.into_iter() + .try_fold(String::new(), |mut a, (mut key, value)| { + if let Some(id_map) = &id_map { + if let Some(new_id) = id_map.get(&key) { + key = new_id.to_owned() + } + } + let key = serde_json::to_string(&key)?; + let value = serde_json::to_string(&value)?; + writeln!(a, "msgid {key}\nmsgstr {value}\n\n",)?; + Ok::<_, anyhow::Error>(a) + })? + ) + .as_bytes(), + )?; + Ok(()) + } + Args::ExportGodotCsv { input_dir, output } => { + let translations = input_dir + .read_dir()? + .flat_map(|e| { + e.map_err(|e| anyhow!("{e}")) + .and_then(|e| { + if e.file_name().to_string_lossy().ends_with(".ini") { + Ok(Some(( + e.path() + .file_stem() + .ok_or(anyhow!("empty filename"))? + .to_string_lossy() + .to_string(), + load_ini(&e.path())?, + ))) + } else { + Ok(None) + } + }) + .transpose() + }) + .collect::>>()?; + + let langs = translations.keys().cloned().collect::>(); + let mut tr_tr = BTreeMap::>::new(); + for (k, v) in translations { + for (kk, vv) in v { + tr_tr.entry(kk).or_default().insert(k.clone(), vv); + } + } + + File::create(output)?.write_all( + tr_tr + .into_iter() + .try_fold(format!("id,{}\n", langs.join(",")), |mut a, (k, v)| { + writeln!( + a, + "{k},{}", + v.values() + .map(serde_json::to_string) + .collect::, serde_json::Error>>()? + .join(",") + )?; + Ok::<_, anyhow::Error>(a) + })? + .as_bytes(), + )?; + Ok(()) + } + Args::ImportOldPo { + reference, + input, + output, + } => { + let reference = read_to_string(reference)?; + let input = read_to_string(input)?; + + let id_reverse = reference + .lines() + .skip(1) + .map(|l| { + l.split_once("=") + .map(|(k, v)| (v, k)) + .ok_or(anyhow!("invalid ini")) + }) + .collect::>>()?; + + let mut outmap = BTreeMap::new(); + let mut mode = 0; + let mut msgid = String::new(); + let mut msgstr = String::new(); + for (i, mut line) in input.lines().enumerate() { + if line.starts_with("#") { + continue; + } + if line.is_empty() { + continue; + } + if let Some(rest) = line.strip_prefix("msgid ") { + if !msgid.is_empty() { + if let Some(id) = id_reverse.get(&msgid.as_str()) { + outmap.insert(id.to_owned(), msgstr.clone()); + } else { + eprintln!("warning: message id {msgid:?} is unknown") + } + } + line = rest; + msgid = String::new(); + mode = 1; + } else if let Some(rest) = line.strip_prefix("msgstr ") { + line = rest; + msgstr = String::new(); + mode = 2; + } else if line.starts_with("msgctxt ") { + mode = 0; + eprintln!("warning: msgctxt not implemented (line {})", i + 1); + continue; + } + let frag = + serde_json::from_str::(line).context(anyhow!("line {}", i + 1))?; + match mode { + 0 => (), + 1 => msgid.push_str(&frag), + 2 => msgstr.push_str(&frag), + _ => unreachable!(), + }; + } + + File::create(output)?.write_all( + outmap + .into_iter() + .try_fold("[hurrycurry]\n".to_string(), |mut a, (k, v)| { + writeln!(a, "{k}={v}")?; + Ok::<_, anyhow::Error>(a) + })? + .as_bytes(), + )?; + + Ok(()) + } + Args::ImportOldPot { input, output } => { + let output_raw = read_to_string(&output).unwrap_or("".to_owned()); + let input = read_to_string(input)?; + + let mut output_flip = output_raw + .lines() + .skip(1) + .map(|l| { + l.split_once("=") + .map(|(k, v)| (v.to_owned(), k.to_owned())) + .ok_or(anyhow!("invalid ini")) + }) + .collect::>>()?; + + let mut id = false; + let mut msgid = String::new(); + for (i, mut line) in input.lines().enumerate() { + if line.starts_with("#") { + continue; + } + if line.is_empty() { + continue; + } + if let Some(rest) = line.strip_prefix("msgid ") { + if !msgid.is_empty() && !output_flip.contains_key(&msgid) { + output_flip.insert(msgid.replace("\n", "\\n"), format!("unknown{i}")); + } + line = rest; + id = true; + msgid = String::new(); + } else if line.starts_with("msgctxt ") || line.starts_with("msgstr ") { + id = false; + continue; + } + if id { + let frag = + serde_json::from_str::(line).context(anyhow!("line {}", i + 1))?; + msgid.push_str(frag.as_str()); + } + } + + let output_unflip = output_flip + .into_iter() + .map(|(v, k)| (k, v)) + .collect::>(); + + File::create(output)?.write_all( + output_unflip + .into_iter() + .try_fold("[hurrycurry]\n".to_string(), |mut a, (k, v)| { + writeln!(a, "{k}={v}")?; + Ok::<_, anyhow::Error>(a) + })? + .as_bytes(), + )?; + + Ok(()) + } + } +} + +fn load_ini(path: &Path) -> Result> { + read_to_string(path)? + .lines() + .skip(1) + .map(|l| { + let (k, v) = l.split_once("=").ok_or(anyhow!("'=' missing"))?; + Ok::<_, anyhow::Error>((k.trim_end().to_owned(), v.trim_start().replace("%n", "\n"))) + }) + .collect() +} diff --git a/server/tools/Cargo.toml b/server/tools/Cargo.toml index 1c427c81..ffffa426 100644 --- a/server/tools/Cargo.toml +++ b/server/tools/Cargo.toml @@ -9,9 +9,6 @@ log = "0.4.28" env_logger = "0.11.8" clap = { version = "4.5.47", features = ["derive"] } hurrycurry-protocol = { path = "../protocol" } -hurrycurry-server = { path = ".." } hurrycurry-locale = { path = "../locale" } hurrycurry-data = { path = "../data" } serde_json = "1.0.145" -serde = { version = "1.0.225", features = ["derive"] } -markup = "0.15.0" diff --git a/server/tools/src/book_html.css b/server/tools/src/book_html.css deleted file mode 100644 index 9171aa5e..00000000 --- a/server/tools/src/book_html.css +++ /dev/null @@ -1,41 +0,0 @@ -/* - 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 . - -*/ -body { - margin: 0px; - background-color: rgb(74, 74, 74); - scroll-behavior: smooth; -} - -.pagegroup { - margin-left: auto; - margin-right: auto; - display: block; - width: 64em; -} - -.page { - display: inline-block; - background-color: antiquewhite; - box-sizing: border-box; - padding: 2em; - margin: 1em; - aspect-ratio: 0.707; - width: 30em; - height: auto; - overflow-y: auto; -} diff --git a/server/tools/src/book_html.rs b/server/tools/src/book_html.rs deleted file mode 100644 index 08a12e88..00000000 --- a/server/tools/src/book_html.rs +++ /dev/null @@ -1,89 +0,0 @@ -/* - 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 . - -*/ - -use crate::diagram_svg::diagram_svg; -use hurrycurry_locale::{ - Locale, - message::{MessageDisplayExt, PLAIN}, -}; -use hurrycurry_protocol::{ - Gamedata, Message, - book::{Book, BookPage, Diagram}, -}; - -pub fn render_html_book(data: &Gamedata, book: &Book, locale: &Locale) -> String { - BookR { book, data, locale }.to_string() -} - -markup::define! { - BookR<'a>(data: &'a Gamedata, book: &'a Book, locale: &'a Locale) { - @markup::doctype() - html { - head { - style { @include_str!("book_html.css").replace(" ", "").replace("\n", "") } - title { "Recipe Book - Hurry Curry!" } - } - body { - @for (index, page) in book.pages.iter().enumerate() { - @PageR { data, locale, page, index } - } - } - } - } - - PageR<'a>(index: usize, data: &'a Gamedata, locale: &'a Locale, page: &'a BookPage) { - section.pagegroup[id=format!("page{index}")] { - @match page { - BookPage::Cover => { - div.page {} - div.page {} - } - BookPage::Recipe { title, description, diagram } => { - div.page { - h1 { @MessageR { data, locale, message: title } } - p { @MessageR { data, locale, message: description } } - } - div.page { - @DiagramR { data, diagram } - } - } - BookPage::Text { .. } => { - div.page {} - div.page {} - } - BookPage::Contents { title, table } => { - div.page { - h1 { @MessageR { data, locale, message: title } } - ol { @for (label, page) in table { - li { a[href=format!("#page{page}")] { @MessageR { data, locale, message: label } } } - }} - } - div.page {} - } - } - } - } - - MessageR<'a>(data: &'a Gamedata, locale: &'a Locale, message: &'a Message) { - @message.display_message(*locale, *data, &PLAIN) - } - - DiagramR<'a>(data: &'a Gamedata, diagram: &'a Diagram) { - @markup::raw(diagram_svg(data, diagram).unwrap()) - } -} diff --git a/server/tools/src/diagram_dot.rs b/server/tools/src/diagram_dot.rs index fb223dbf..1701f4ab 100644 --- a/server/tools/src/diagram_dot.rs +++ b/server/tools/src/diagram_dot.rs @@ -16,7 +16,6 @@ */ -use crate::diagram_svg::node_color; use anyhow::{Result, bail}; use hurrycurry_protocol::{ Gamedata, Message, @@ -91,6 +90,16 @@ pub fn diagram_dot(data: &Gamedata, diagram: &Diagram, use_position: bool) -> Re Ok(out) } +fn node_color(node: &DiagramNode) -> &'static str { + match node.style { + NodeStyle::FinalProduct => "#555", + NodeStyle::IntermediateProduct => "#333", + NodeStyle::ProcessActive => "#47c42b", + NodeStyle::ProcessPassive => "#c4a32b", + NodeStyle::ProcessInstant => "#5452d8", + } +} + fn node_style(attrs: &mut Vec, node: &DiagramNode) { let shape = match node.style { NodeStyle::FinalProduct => "circle", diff --git a/server/tools/src/diagram_svg.rs b/server/tools/src/diagram_svg.rs deleted file mode 100644 index d006ce78..00000000 --- a/server/tools/src/diagram_svg.rs +++ /dev/null @@ -1,152 +0,0 @@ -/* - 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 . - -*/ - -use anyhow::Result; -use hurrycurry_protocol::{ - Gamedata, Message, - book::{Diagram, DiagramNode, NodeStyle}, - glam::Vec2, -}; -use std::fmt::Write; - -const SIZE: f32 = 64.; -const HSIZE: f32 = SIZE / 2.; - -pub fn diagram_svg(data: &Gamedata, diagram: &Diagram) -> Result { - let mut out = String::new(); - - let (vb_min, vb_max) = diagram - .nodes - .iter() - .map(|n| (n.position, n.position)) - .reduce(|(xa, xb), (ya, yb)| (xa.min(ya), xb.max(yb))) - .unwrap(); - - writeln!( - out, - r#""#, - vb_min.x - HSIZE, - vb_min.y - HSIZE, - vb_max.x + HSIZE, - vb_max.y + HSIZE, - vb_max.x - vb_min.x + SIZE, - vb_max.y - vb_min.y + SIZE, - )?; - - for node in &diagram.nodes { - match node.label { - Message::Translation { .. } => {} - Message::Text(_) => {} - Message::Item(item_index) => { - image_node( - &mut out, - node, - format!("items/{}.png", data.item_name(item_index)), - )?; - } - Message::Tile(tile_index) => { - image_node( - &mut out, - node, - format!("tiles/{}.png", data.tile_name(tile_index)), - )?; - } - } - } - for edge in &diagram.edges { - let src_node = &diagram.nodes[edge.src]; - let dst_node = &diagram.nodes[edge.dst]; - let src = node_edge_connect_pos(src_node, dst_node); - let mut dst = node_edge_connect_pos(dst_node, src_node); - - let dir = (src - dst).normalize_or_zero(); - let tip0 = dst; - let tip1 = dst + (dir + dir.perp() * 0.5) * 10.; - let tip2 = dst + (dir + dir.perp() * -0.5) * 10.; - dst += dir * 5.; // prevent miter line cap from peeking out - - // line path - writeln!( - out, - r#""#, - src.x, src.y, dst.x, dst.y, - )?; - // tip path - writeln!( - out, - r#""#, - tip0.x, tip0.y, tip1.x, tip1.y, tip2.x, tip2.y - )?; - } - - writeln!(out, "")?; - Ok(out) -} - -fn node_edge_connect_pos(src: &DiagramNode, dst: &DiagramNode) -> Vec2 { - let dir = (dst.position - src.position).normalize_or_zero(); - if matches!( - src.style, - NodeStyle::FinalProduct | NodeStyle::IntermediateProduct - ) { - src.position + dir * HSIZE // circle - } else { - src.position + dir / dir.y.abs() * HSIZE // square (only +Y and -Y sides) - } -} - -pub(crate) fn node_color(node: &DiagramNode) -> &'static str { - match node.style { - NodeStyle::FinalProduct => "#555", - NodeStyle::IntermediateProduct => "#333", - NodeStyle::ProcessActive => "#47c42b", - NodeStyle::ProcessPassive => "#c4a32b", - NodeStyle::ProcessInstant => "#5452d8", - } -} - -fn image_node(out: &mut String, node: &DiagramNode, path: String) -> Result<()> { - if matches!( - node.style, - NodeStyle::FinalProduct | NodeStyle::IntermediateProduct - ) { - writeln!( - out, - r#""#, - node.position.x, - node.position.y, - HSIZE, - node_color(node) - )?; - } else { - writeln!( - out, - r#""#, - node.position.x - HSIZE, - node.position.y - HSIZE, - node_color(node) - )?; - } - writeln!( - out, - r#""#, - node.position.x - HSIZE, - node.position.y - HSIZE, - )?; - Ok(()) -} diff --git a/server/tools/src/main.rs b/server/tools/src/main.rs index 8d8e4fd0..07c5d45b 100644 --- a/server/tools/src/main.rs +++ b/server/tools/src/main.rs @@ -16,17 +16,13 @@ */ -pub mod book_html; pub mod diagram_dot; -pub mod diagram_svg; pub mod graph; pub mod graph_summary; pub mod map_linter; use crate::{ - book_html::render_html_book, diagram_dot::{diagram_dot, diagram_dot_svg}, - diagram_svg::diagram_svg, graph::graph, graph_summary::graph_summary, map_linter::check_map, @@ -34,10 +30,9 @@ use crate::{ use anyhow::Result; use clap::Parser; use hurrycurry_data::{ - book::{book, diagram_layout::diagram_layout, print_book, recipe_diagram::recipe_diagram}, + book::{diagram_layout::diagram_layout, recipe_diagram::recipe_diagram}, index::DataIndex, }; -use hurrycurry_locale::FALLBACK_LOCALE; #[derive(Parser)] enum Action { @@ -46,12 +41,8 @@ enum Action { GraphSingle { out: String, #[arg(short)] - custom_svg: bool, - #[arg(short)] dot_out: bool, }, - Book, - BookHtml, MapDemands { map: String, }, @@ -73,11 +64,7 @@ fn main() -> Result<()> { match action { Action::Graph => graph()?, Action::GraphSummary => graph_summary()?, - Action::GraphSingle { - out, - custom_svg, - dot_out, - } => { + Action::GraphSingle { out, dot_out } => { let mut index = DataIndex::default(); index.reload()?; let (data, serverdata) = index.generate("5star")?; @@ -85,28 +72,12 @@ fn main() -> Result<()> { let mut diagram = recipe_diagram(&data, &serverdata, &[out])?; let out = if dot_out { diagram_dot(&data, &diagram, false)? - } else if custom_svg { - diagram_layout(&mut diagram)?; - diagram_svg(&data, &diagram)? } else { diagram_layout(&mut diagram)?; diagram_dot_svg(&data, &diagram)? }; println!("{out}"); } - Action::Book => { - let mut index = DataIndex::default(); - index.reload()?; - let (data, serverdata) = index.generate("5star")?; - print_book(&data, &serverdata)? - } - Action::BookHtml => { - let mut index = DataIndex::default(); - index.reload()?; - let (data, serverdata) = index.generate("5star")?; - let book = book(&data, &serverdata)?; - println!("{}", render_html_book(&data, &book, &FALLBACK_LOCALE)); - } Action::MapDemands { map } => { let mut index = DataIndex::default(); index.reload()?; -- cgit v1.3