diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-01-24 18:43:16 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-01-24 18:43:16 +0100 |
| commit | 9cb4599f5ed6d0933c2385152ea4caf6465baef8 (patch) | |
| tree | 86585891a755d9939f79d2ef61add1bc65469d8a | |
| parent | 15f864d3757fbfc5d5f9b33a54bfeae448468468 (diff) | |
| download | hurrycurry-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.rs | 5 | ||||
| -rw-r--r-- | server/data/src/index.rs | 97 | ||||
| -rw-r--r-- | server/data/src/lib.rs | 370 | ||||
| -rw-r--r-- | server/data/src/recipes.rs | 125 | ||||
| -rw-r--r-- | server/data/src/registry.rs | 6 |
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, ®)?; + // 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, ®)?; 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(®.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 { |