aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-01-24 18:43:16 +0100
committermetamuffin <metamuffin@disroot.org>2026-01-24 18:43:16 +0100
commit9cb4599f5ed6d0933c2385152ea4caf6465baef8 (patch)
tree86585891a755d9939f79d2ef61add1bc65469d8a
parent15f864d3757fbfc5d5f9b33a54bfeae448468468 (diff)
downloadhurrycurry-9cb4599f5ed6d0933c2385152ea4caf6465baef8.tar
hurrycurry-9cb4599f5ed6d0933c2385152ea4caf6465baef8.tar.bz2
hurrycurry-9cb4599f5ed6d0933c2385152ea4caf6465baef8.tar.zst
modify data code to load tile stack maps
-rw-r--r--server/book-export/src/main.rs5
-rw-r--r--server/data/src/index.rs97
-rw-r--r--server/data/src/lib.rs370
-rw-r--r--server/data/src/recipes.rs125
-rw-r--r--server/data/src/registry.rs6
5 files changed, 303 insertions, 300 deletions
diff --git a/server/book-export/src/main.rs b/server/book-export/src/main.rs
index d89aa3d4..ff9445c2 100644
--- a/server/book-export/src/main.rs
+++ b/server/book-export/src/main.rs
@@ -21,7 +21,7 @@ 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_data::{book::book, build_data};
use hurrycurry_locale::Locale;
use std::{fs::File, io::Write, path::PathBuf};
@@ -49,8 +49,7 @@ fn main() -> Result<()> {
env_logger::init_from_env("LOG");
let args = Args::parse();
- let index = DataIndex::new("data".into())?;
- let (data, serverdata) = index.generate(&args.map)?;
+ let (data, serverdata) = build_data("data".as_ref(), &args.map, true)?;
let book = book(&data, &serverdata)?;
diff --git a/server/data/src/index.rs b/server/data/src/index.rs
deleted file mode 100644
index 7eed1699..00000000
--- a/server/data/src/index.rs
+++ /dev/null
@@ -1,97 +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 <https://www.gnu.org/licenses/>.
-
-*/
-
-use anyhow::{Context, Result, anyhow, bail};
-use hurrycurry_protocol::{Gamedata, MapMetadata};
-use log::debug;
-use serde::Deserialize;
-use std::{
- collections::{HashMap, HashSet},
- fs::{File, read_to_string},
- path::PathBuf,
-};
-
-use crate::{MapDecl, Serverdata, book::book, build_data};
-
-#[derive(Debug, Deserialize)]
-pub struct DataIndex {
- #[serde(skip)]
- pub path: PathBuf,
- pub maps: HashMap<String, MapMetadata>,
- pub recipes: HashSet<String>,
-}
-
-impl DataIndex {
- pub fn new(path: PathBuf) -> Result<Self> {
- let mut s = Self {
- path,
- maps: HashMap::new(),
- recipes: HashSet::new(),
- };
- s.reload()?;
- Ok(s)
- }
- pub fn reload(&mut self) -> Result<()> {
- let path = self.path.clone();
- *self = serde_yaml_ng::from_reader(
- File::open(self.path.join("index.yaml")).context("Failed opening data index")?,
- )?;
- self.path = path;
- Ok(())
- }
-
- pub fn read_map(&self, name: &str) -> Result<String> {
- // Scary!
- if name.contains("..") || name.starts_with("/") || name.contains("//") {
- bail!("illegal map path");
- }
- let path = self.path.join(format!("maps/{name}.yaml"));
- Ok(read_to_string(path)?)
- }
- pub fn read_recipes(&self, name: &str) -> Result<String> {
- if !self.recipes.contains(name) {
- bail!("unknown recipes: {name:?}");
- }
- let path = self.path.join(format!("recipes/{name}.yaml"));
- Ok(read_to_string(path)?)
- }
- pub fn generate(&self, map: &str) -> Result<(Gamedata, Serverdata)> {
- debug!("Loading map {map}...");
- let map_in: MapDecl = serde_yaml_ng::from_str(
- &self
- .read_map(map)
- .context(anyhow!("Failed to read map file ({map})"))?,
- )
- .context(anyhow!("Failed to parse map file ({map})"))?;
- let recipes_in = serde_yaml_ng::from_str(
- &self
- .read_recipes(map_in.recipes.as_deref().unwrap_or("default"))
- .context("Failed read recipe file")?,
- )
- .context("Failed to parse recipe file")?;
-
- let data = build_data(&self.maps, map.to_string(), map_in, recipes_in)?;
- debug!("Done.");
- Ok(data)
- }
- pub fn generate_with_book(&self, map: &str) -> Result<(Gamedata, Serverdata)> {
- let (gd, mut sd) = self.generate(map)?;
- sd.book = book(&gd, &sd).context("within book")?;
- Ok((gd, sd))
- }
-}
diff --git a/server/data/src/lib.rs b/server/data/src/lib.rs
index 80b2f75b..faaf3980 100644
--- a/server/data/src/lib.rs
+++ b/server/data/src/lib.rs
@@ -19,77 +19,58 @@
pub mod book;
pub mod entities;
pub mod filter_demands;
-pub mod index;
+pub mod recipes;
pub mod registry;
-use anyhow::{Result, anyhow, bail};
+use crate::{
+ book::book,
+ entities::EntityDecl,
+ recipes::{RecipeDecl, load_recipes},
+ registry::{ItemTileRegistry, filter_unused_tiles_and_items},
+};
+use anyhow::{Context, Result, anyhow, bail};
use clap::Parser;
use filter_demands::filter_demands_and_recipes;
use hurrycurry_protocol::{
- Demand, Gamedata, GamedataFlags, ItemIndex, MapMetadata, Recipe, TileIndex,
+ Gamedata, GamedataFlags, ItemIndex, MapMetadata, Recipe, TileIndex,
book::Book,
glam::{IVec2, Vec2},
};
use log::debug;
-use serde::{Deserialize, Serialize};
+use serde::Deserialize;
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
+ fs::read_to_string,
+ path::Path,
time::Duration,
};
-use crate::{
- entities::EntityDecl,
- registry::{ItemTileRegistry, filter_unused_tiles_and_items},
-};
-
-#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
-#[serde(rename_all = "snake_case")]
-pub enum RecipeDeclAction {
- #[default]
- Never,
- Passive,
- Active,
- Instant,
- Demand,
-}
-
-#[rustfmt::skip]
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct RecipeDecl {
- tile: Option<String>,
- #[serde(default)] inputs: Vec<String>,
- #[serde(default)] outputs: Vec<String>,
- #[serde(default)] action: RecipeDeclAction,
- #[serde(default)] warn: bool,
- revert_duration: Option<f32>,
- duration: Option<f32>,
- points: Option<i64>,
- group: Option<String>,
- #[serde(default)] group_hidden: bool,
+#[derive(Debug, Deserialize)]
+pub struct DataIndex {
+ pub maps: HashMap<String, MapMetadata>,
+ pub recipes: HashSet<String>,
}
#[rustfmt::skip]
#[derive(Debug, Clone, Deserialize)]
pub struct MapDecl {
map: Vec<String>,
- tiles: HashMap<char, String>,
- #[serde(default)] recipes: Option<String>,
+ #[serde(default)] tiles: HashMap<char, String>,
+ #[serde(default)] use_palettes: Vec<String>,
+ #[serde(default = "default_recipes")] recipes: String,
#[serde(default)] hand_count: Option<usize>,
#[serde(default)] entities: Vec<EntityDecl>,
#[serde(default)] score_baseline: i64,
#[serde(default)] default_timer: Option<u64>,
#[serde(default)] flags: GamedataFlags,
}
+fn default_recipes() -> String {
+ "default".to_string()
+}
#[derive(Parser)]
struct TileArgs {
- tile_name: String,
- #[clap(short = 'c', long)]
- collider: bool,
- #[clap(short = 'x', long)]
- exclusive: bool,
- #[clap(short = 'w', long)]
- walkable: bool,
+ tiles: Vec<String>,
#[clap(long)]
book: bool,
#[clap(long)]
@@ -104,14 +85,6 @@ struct TileArgs {
demand_sink: bool,
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct DemandDecl {
- from: String,
- to: Option<String>,
- duration: f32,
- points: i64,
-}
-
#[derive(Debug, Clone, Default)]
#[rustfmt::skip]
pub struct Serverdata {
@@ -125,35 +98,48 @@ pub struct Serverdata {
pub recipe_groups: BTreeMap<String, BTreeSet<ItemIndex>>,
}
-fn build_data(
- maps: &HashMap<String, MapMetadata>,
- map_name: String,
- map_in: MapDecl,
- recipes_in: Vec<RecipeDecl>,
+pub fn build_data(
+ data_path: &Path,
+ map_name: &str,
+ generate_book: bool,
) -> Result<(Gamedata, Serverdata)> {
debug!("Preparing gamedata for {map_name}");
- let reg = ItemTileRegistry::default();
- let (mut recipes, mut demands, recipe_groups) = load_recipes(recipes_in, &reg)?;
+ // Load index
+ let index =
+ read_to_string(data_path.join("index.yaml")).context("Failed reading data index")?;
+ let index = serde_yaml_ng::from_str::<DataIndex>(&index)?;
+
+ // Load map
+ if map_name.contains("..") || map_name.starts_with("/") || map_name.contains("//") {
+ bail!("illegal map path");
+ }
+ let map_in = read_to_string(data_path.join(format!("maps/{map_name}.yaml")))
+ .context("Failed reading map file")?;
+ let map_in = serde_yaml_ng::from_str::<MapDecl>(&map_in)?;
- let mut tile_specs = BTreeMap::new();
- for (char, tile_spec_raw) in map_in.tiles {
- let mut toks = shlex::split(&tile_spec_raw).ok_or(anyhow!("tile spec quoting invalid"))?;
- toks.insert(0, "tile-spec".to_string()); // exe name
- tile_specs.insert(char, TileArgs::try_parse_from(toks)?);
+ // Load recipes
+ if !index.recipes.contains(&map_in.recipes) {
+ bail!("unknown recipes: {:?}", map_in.recipes);
}
+ let recipes_in = read_to_string(data_path.join(format!("recipes/{}.yaml", map_in.recipes)))?;
+ let recipes_in = serde_yaml_ng::from_str::<Vec<RecipeDecl>>(&recipes_in)?;
+
+ // Load tile flags
+ let tile_flags =
+ read_to_string(data_path.join("tiles.yaml")).context("Failed reading tile flags")?;
+ let tile_flags = serde_yaml_ng::from_str::<HashMap<String, String>>(&tile_flags)?;
+
+ let palette = load_palette(data_path, &map_in)?;
+
+ let reg = ItemTileRegistry::default();
+ let (mut recipes, mut demands, recipe_groups) = load_recipes(recipes_in, &reg)?;
let mut entities = Vec::new();
let mut chef_spawn = None;
let mut customer_spawn = None;
let mut initial_map = HashMap::new();
- let mut tiles_used = HashSet::new();
- let mut items_used = HashSet::new();
- let mut tile_walkable = HashSet::new();
- let mut exclusive_tiles = BTreeMap::<TileIndex, HashSet<ItemIndex>>::new();
- let mut tile_placeable_items = BTreeMap::new();
- let mut tile_placeable_any = HashSet::new();
- let mut tile_interactable_empty = HashSet::new();
+
for (y, line) in map_in.map.iter().enumerate() {
for (x, char) in line.chars().enumerate() {
if char == ' ' {
@@ -161,36 +147,32 @@ fn build_data(
}
let pos = IVec2::new(x as i32, y as i32);
- let tile_spec = tile_specs
+ let ts = palette
.get(&char)
.ok_or(anyhow!("tile {char} is undefined"))?;
- let tile = reg.register_tile(tile_spec.tile_name.clone());
- tiles_used.insert(tile);
- let item = tile_spec.item.clone().map(|i| reg.register_item(i));
- items_used.extend(item);
- initial_map.insert(pos, (vec![tile], item));
+ let tiles = ts
+ .tiles
+ .iter()
+ .cloned()
+ .map(|t| reg.register_tile(t))
+ .collect();
+ let item = ts.item.clone().map(|i| reg.register_item(i));
+ initial_map.insert(pos, (tiles, item));
- if tile_spec.chef_spawn {
+ if ts.chef_spawn {
chef_spawn = Some(pos.as_vec2() + Vec2::splat(0.5));
}
- if tile_spec.customer_spawn {
+ if ts.customer_spawn {
customer_spawn = Some(pos.as_vec2() + Vec2::splat(0.5));
}
- if tile_spec.walkable {
- tile_walkable.insert(tile);
- }
- if tile_spec.walkable || tile_spec.collider || tile_spec.exclusive {
- exclusive_tiles.entry(tile).or_default().extend(item);
- }
- if tile_spec.book {
+ if ts.book {
entities.push(EntityDecl::Book { pos });
- tile_interactable_empty.insert(tile); // if it doesnt have a dedicated tile all of its kind will be interactable
}
- if tile_spec.demand_sink {
+ if ts.demand_sink {
entities.push(EntityDecl::DemandSink { pos });
}
- if let Some(off) = &tile_spec.conveyor {
+ if let Some(off) = &ts.conveyor {
let (x, y) = off
.split_once(",")
.ok_or(anyhow!("conveyor offset invalid format"))?;
@@ -206,12 +188,6 @@ fn build_data(
let chef_spawn = chef_spawn.ok_or(anyhow!("map has no chef spawn"))?;
- for tile in tile_specs.values() {
- if !tiles_used.contains(&reg.register_tile(tile.tile_name.clone())) {
- bail!("tile {:?} is unused", tile.tile_name)
- }
- }
-
for mut e in map_in.entities.clone() {
match &mut e {
EntityDecl::Customers { unknown_order, .. } => {
@@ -223,7 +199,6 @@ fn build_data(
} => {
*tag_item = reg.register_item("lettuce".to_owned());
*blocker_tile = reg.register_tile("conveyor".to_owned());
- exclusive_tiles.entry(*blocker_tile).or_default();
}
EntityDecl::PlayerPortalPair {
in_tile,
@@ -234,7 +209,6 @@ fn build_data(
*in_tile = reg.register_tile("black-hole".to_owned());
*neutral_tile = reg.register_tile("grey-hole".to_owned());
*out_tile = reg.register_tile("white-hole".to_owned());
- tile_walkable.extend([*in_tile, *neutral_tile, *out_tile]);
}
EntityDecl::CtfMinigame {
items,
@@ -247,35 +221,10 @@ fn build_data(
}
entities.push(e);
}
- debug!("{} entites created", entities.len());
-
- filter_demands_and_recipes(&tiles_used, &items_used, &mut demands, &mut recipes);
+ debug!("{} entities created", entities.len());
- let mut maps = maps
- .iter()
- .filter(|(_, v)| v.players > 0)
- .map(|(k, v)| (k.to_owned(), v.to_owned()))
- .collect::<Vec<(String, MapMetadata)>>();
- maps.sort_unstable_by_key(|(_, m)| m.difficulty);
- maps.sort_by_key(|(_, m)| m.players);
-
- for (tile, used_items) in exclusive_tiles {
- let whitelist = recipes
- .iter()
- .filter(|r| r.tile() == Some(tile))
- .flat_map(|e| e.inputs())
- .chain(used_items)
- .collect();
- let int_empty = recipes
- .iter()
- .any(|r| r.tile() == Some(tile) && r.inputs().next().is_none());
- tile_placeable_items.insert(tile, whitelist);
- if int_empty {
- tile_interactable_empty.insert(tile);
- }
- }
-
- let (item_names, tile_names) = reg.finish();
+ filter_demands_and_recipes(&initial_map, &mut demands, &mut recipes);
+ let (items, tiles) = reg.finish();
let default_timer = if map_name.ends_with("lobby") {
None
@@ -284,17 +233,24 @@ fn build_data(
};
let mut data = Gamedata {
- current_map: map_name,
- maps,
- tile_collide: tile_walkable,
- tile_placeable_items,
- tile_placeable_any,
- tile_interactable_empty,
+ current_map: map_name.to_string(),
+ maps: map_listing(&index),
+ tile_collide: tiles_flagged(&tile_flags, &tiles, 'c'),
+ tile_placeable_items: tile_placeable_items(
+ &initial_map,
+ &recipes,
+ tiles_flagged(&tile_flags, &tiles, 'x'),
+ ),
+ tile_placeable_any: tiles_flagged(&tile_flags, &tiles, 'a'),
+ tile_interactable_empty: tiles_flagged(&tile_flags, &tiles, 'e')
+ .union(&tile_interactable_empty_bc_recipe(&recipes))
+ .copied()
+ .collect(),
flags: map_in.flags,
recipes,
- item_names,
+ item_names: items,
demands,
- tile_names,
+ tile_names: tiles,
bot_algos: vec![
"waiter".to_string(),
"simple".to_string(),
@@ -315,81 +271,95 @@ fn build_data(
};
filter_unused_tiles_and_items(&mut data, &mut serverdata);
+ if generate_book {
+ serverdata.book = book(&data, &serverdata).context("within book")?;
+ }
+
Ok((data, serverdata))
}
-#[allow(clippy::type_complexity)]
-fn load_recipes(
- recipes_in: Vec<RecipeDecl>,
- reg: &ItemTileRegistry,
-) -> Result<(
- Vec<Recipe>,
- Vec<Demand>,
- BTreeMap<String, BTreeSet<ItemIndex>>,
-)> {
- let mut recipes = Vec::new();
- let mut demands = Vec::new();
- let mut recipe_groups = BTreeMap::<String, BTreeSet<ItemIndex>>::new();
+fn tile_placeable_items(
+ initial_map: &HashMap<IVec2, (Vec<TileIndex>, Option<ItemIndex>)>,
+ recipes: &[Recipe],
+ extiles: HashSet<TileIndex>,
+) -> BTreeMap<TileIndex, HashSet<ItemIndex>> {
+ let mut tile_placeable_items = BTreeMap::new();
+ for tile in extiles {
+ let initially_placed = initial_map
+ .values()
+ .filter(|(t, _)| t.contains(&tile))
+ .flat_map(|(_, i)| i)
+ .copied();
+ let used_in_recipe = recipes
+ .iter()
+ .filter(|r| r.tile() == Some(tile))
+ .flat_map(|e| e.inputs());
+ tile_placeable_items.insert(tile, used_in_recipe.chain(initially_placed).collect());
+ }
+ tile_placeable_items
+}
- for mut r in recipes_in {
- #[cfg(feature = "fast_recipes")]
- match r.action {
- RecipeDeclAction::Passive | RecipeDeclAction::Active => {
- if !r.warn {
- r.duration = Some(0.5)
- }
- }
- _ => (),
- }
+fn tile_interactable_empty_bc_recipe(recipes: &[Recipe]) -> HashSet<TileIndex> {
+ recipes
+ .iter()
+ .filter(|r| r.inputs().next().is_none())
+ .flat_map(|r| r.tile())
+ .collect()
+}
- let r2 = r.clone();
- let mut inputs = r.inputs.into_iter().map(|i| reg.register_item(i));
- let mut outputs = r.outputs.into_iter().map(|o| reg.register_item(o));
- let tile = r.tile.map(|t| reg.register_tile(t));
- if let Some(g) = r.group
- && !r.group_hidden
- {
- recipe_groups.entry(g).or_default().extend(inputs.clone());
- }
- match r.action {
- RecipeDeclAction::Never => {}
- RecipeDeclAction::Passive => recipes.push(Recipe::Passive {
- speed: 1. / r.duration.ok_or(anyhow!("duration for passive missing"))?,
- warn: r.warn,
- tile,
- revert_speed: r.revert_duration.map(|d| 1. / d),
- input: inputs
- .next()
- .ok_or(anyhow!("passive recipe without input"))?,
- output: outputs.next(),
- }),
- RecipeDeclAction::Active => recipes.push(Recipe::Active {
- speed: 1. / r.duration.ok_or(anyhow!("duration for active missing"))?,
- tile,
- input: inputs
- .next()
- .ok_or(anyhow!("active recipe without input"))?,
- outputs: [outputs.next(), outputs.next()],
- }),
- RecipeDeclAction::Instant => {
- recipes.push(Recipe::Instant {
- points: r.points.take().unwrap_or(0),
- tile,
- inputs: [inputs.next(), inputs.next()],
- outputs: [outputs.next(), outputs.next()],
- });
+fn map_listing(index: &DataIndex) -> Vec<(String, MapMetadata)> {
+ let mut maps = index
+ .maps
+ .iter()
+ .filter(|(_, v)| v.players > 0)
+ .map(|(k, v)| (k.to_owned(), v.to_owned()))
+ .collect::<Vec<(String, MapMetadata)>>();
+ maps.sort_unstable_by_key(|(_, m)| m.difficulty);
+ maps.sort_by_key(|(_, m)| m.players);
+ maps
+}
+
+fn load_palette(data_path: &Path, map_in: &MapDecl) -> Result<HashMap<char, TileArgs>> {
+ // Load palette
+ let palettes =
+ read_to_string(data_path.join("palettes.yaml")).context("Failed reading palettes")?;
+ let palettes = serde_yaml_ng::from_str::<HashMap<String, HashMap<char, String>>>(&palettes)?;
+
+ let mut raw_args = HashMap::new();
+ for p in &map_in.use_palettes {
+ raw_args.extend(
+ palettes
+ .get(p)
+ .cloned()
+ .ok_or(anyhow!("palette {p:?} is undefined"))?,
+ )
+ }
+ raw_args.extend(map_in.tiles.clone());
+
+ let mut palette = HashMap::new();
+ for (k, raw) in raw_args {
+ let mut toks = shlex::split(&raw).ok_or(anyhow!("tile stack quoting invalid"))?;
+ toks.insert(0, "tilestack".to_string()); // exe name
+ let args = TileArgs::try_parse_from(toks)
+ .context(anyhow!("tile declaration for {k:?} is invalid"))?;
+ palette.insert(k, args);
+ }
+
+ Ok(palette)
+}
+
+fn tiles_flagged(
+ tile_flags: &HashMap<String, String>,
+ tiles: &[String],
+ flag: char,
+) -> HashSet<TileIndex> {
+ let mut out = HashSet::new();
+ for (i, tile) in tiles.iter().enumerate() {
+ if let Some(flags) = tile_flags.get(tile) {
+ if flags.contains(flag) {
+ out.insert(TileIndex(i));
}
- RecipeDeclAction::Demand => demands.push(Demand {
- input: inputs.next().ok_or(anyhow!("demand needs inputs"))?,
- output: outputs.next(),
- duration: r.duration.unwrap_or(10.),
- points: 0, // assigned later when filtering
- }),
}
- assert_eq!(inputs.next(), None, "{r2:?} inputs left over");
- assert_eq!(outputs.next(), None, "{r2:?} outputs left over");
- assert_eq!(r.points, None, "points specified where not possible")
}
-
- Ok((recipes, demands, recipe_groups))
+ out
}
diff --git a/server/data/src/recipes.rs b/server/data/src/recipes.rs
new file mode 100644
index 00000000..e4d68a7d
--- /dev/null
+++ b/server/data/src/recipes.rs
@@ -0,0 +1,125 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright (C) 2026 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::registry::ItemTileRegistry;
+use anyhow::{Result, anyhow};
+use hurrycurry_protocol::{Demand, ItemIndex, Recipe};
+use serde::{Deserialize, Serialize};
+use std::collections::{BTreeMap, BTreeSet};
+
+#[rustfmt::skip]
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct RecipeDecl {
+ tile: Option<String>,
+ #[serde(default)] inputs: Vec<String>,
+ #[serde(default)] outputs: Vec<String>,
+ #[serde(default)] action: RecipeDeclAction,
+ #[serde(default)] warn: bool,
+ revert_duration: Option<f32>,
+ duration: Option<f32>,
+ points: Option<i64>,
+ group: Option<String>,
+ #[serde(default)] group_hidden: bool,
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum RecipeDeclAction {
+ #[default]
+ Never,
+ Passive,
+ Active,
+ Instant,
+ Demand,
+}
+
+#[allow(clippy::type_complexity)]
+pub(crate) fn load_recipes(
+ recipes_in: Vec<RecipeDecl>,
+ reg: &ItemTileRegistry,
+) -> Result<(
+ Vec<Recipe>,
+ Vec<Demand>,
+ BTreeMap<String, BTreeSet<ItemIndex>>,
+)> {
+ let mut recipes = Vec::new();
+ let mut demands = Vec::new();
+ let mut recipe_groups = BTreeMap::<String, BTreeSet<ItemIndex>>::new();
+
+ for mut r in recipes_in {
+ #[cfg(feature = "fast_recipes")]
+ match r.action {
+ RecipeDeclAction::Passive | RecipeDeclAction::Active => {
+ if !r.warn {
+ r.duration = Some(0.5)
+ }
+ }
+ _ => (),
+ }
+
+ let r2 = r.clone();
+ let mut inputs = r.inputs.into_iter().map(|i| reg.register_item(i));
+ let mut outputs = r.outputs.into_iter().map(|o| reg.register_item(o));
+ let tile = r.tile.map(|t| reg.register_tile(t));
+ if let Some(g) = r.group
+ && !r.group_hidden
+ {
+ recipe_groups.entry(g).or_default().extend(inputs.clone());
+ }
+ match r.action {
+ RecipeDeclAction::Never => {}
+ RecipeDeclAction::Passive => recipes.push(Recipe::Passive {
+ speed: 1. / r.duration.ok_or(anyhow!("duration for passive missing"))?,
+ warn: r.warn,
+ tile,
+ revert_speed: r.revert_duration.map(|d| 1. / d),
+ input: inputs
+ .next()
+ .ok_or(anyhow!("passive recipe without input"))?,
+ output: outputs.next(),
+ }),
+ RecipeDeclAction::Active => recipes.push(Recipe::Active {
+ speed: 1. / r.duration.ok_or(anyhow!("duration for active missing"))?,
+ tile,
+ input: inputs
+ .next()
+ .ok_or(anyhow!("active recipe without input"))?,
+ outputs: [outputs.next(), outputs.next()],
+ }),
+ RecipeDeclAction::Instant => {
+ recipes.push(Recipe::Instant {
+ points: r.points.take().unwrap_or(0),
+ tile,
+ inputs: [inputs.next(), inputs.next()],
+ outputs: [outputs.next(), outputs.next()],
+ });
+ }
+ RecipeDeclAction::Demand => demands.push(Demand {
+ input: inputs.next().ok_or(anyhow!("demand needs inputs"))?,
+ output: outputs.next(),
+ duration: r.duration.unwrap_or(10.),
+ points: 0, // assigned later when filtering
+ }),
+ }
+ assert_eq!(inputs.next(), None, "{r2:?} inputs left over");
+ assert_eq!(outputs.next(), None, "{r2:?} outputs left over");
+ assert_eq!(r.points, None, "points specified where not possible")
+ }
+
+ Ok((recipes, demands, recipe_groups))
+}
diff --git a/server/data/src/registry.rs b/server/data/src/registry.rs
index 4de672d0..9b592003 100644
--- a/server/data/src/registry.rs
+++ b/server/data/src/registry.rs
@@ -105,6 +105,12 @@ pub(crate) fn filter_unused_tiles_and_items(data: &mut Gamedata, serverdata: &mu
};
}
+ data.tile_placeable_items
+ .iter_mut()
+ .for_each(|(_, is)| is.retain(|i| used_items.contains(i)));
+ data.tile_placeable_items
+ .retain(|t, _| used_tiles.contains(t));
+
let mut item_names = Vec::new();
let mut item_map = HashMap::new();
for item in used_items {