From 6ed804c453cb21e8ff578b14052440f561a6cf72 Mon Sep 17 00:00:00 2001 From: Paris DOUADY Date: Fri, 12 Jan 2024 00:18:37 +0100 Subject: [PATCH] wip --- Cargo.lock | 51 +++ assets/items.json | 81 +---- assets_gui/src/companies.rs | 4 +- base/companies.lua | 0 base/data.lua | 1 + base/items.lua | 113 +++++++ common/src/lib.rs | 10 + common/src/timestep.rs | 2 +- geom/Cargo.toml | 7 +- geom/src/aabb3.rs | 2 +- geom/src/v2.rs | 24 ++ geom/src/v3.rs | 35 ++ .../src/gui/inspect/inspect_building.rs | 4 +- prototypes/Cargo.toml | 11 +- prototypes/src/company.rs | 204 ++++++++++++ prototypes/src/item.rs | 39 +++ prototypes/src/lib.rs | 303 ++++++++++++++---- prototypes/src/prototype_init.lua | 12 + simulation/src/economy/item.rs | 85 ----- simulation/src/economy/market.rs | 24 +- simulation/src/economy/mod.rs | 26 +- simulation/src/souls/goods_company.rs | 155 ++------- 22 files changed, 793 insertions(+), 400 deletions(-) create mode 100644 base/companies.lua create mode 100644 base/data.lua create mode 100644 base/items.lua create mode 100644 prototypes/src/company.rs create mode 100644 prototypes/src/item.rs create mode 100644 prototypes/src/prototype_init.lua diff --git a/Cargo.lock b/Cargo.lock index af538ce37..9170c6409 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,16 @@ dependencies = [ "objc2", ] +[[package]] +name = "bstr" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -1144,6 +1154,7 @@ dependencies = [ "flat_spatial", "fnv", "inline_tweak", + "mlua", "ordered-float", "serde", ] @@ -1702,6 +1713,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "luau0-src" +version = "0.7.11+luau606" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ffc4945ee953a33cb2b331e00b19e11275fc105c8ac8a977c810597d790f08" +dependencies = [ + "cc", +] + [[package]] name = "mach2" version = "0.4.2" @@ -1818,6 +1838,32 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" +[[package]] +name = "mlua" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069264935e816c85884b99e88c8b408d6d92e40ae8760f726c983526a53546b5" +dependencies = [ + "bstr", + "libloading 0.8.1", + "mlua-sys", + "num-traits", + "once_cell", + "rustc-hash", +] + +[[package]] +name = "mlua-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4655631a02e3739d014951291ecfa08db49c4da3f7f8c6f3931ed236af5dd78e" +dependencies = [ + "cc", + "cfg-if", + "luau0-src", + "pkg-config", +] + [[package]] name = "naga" version = "0.14.2" @@ -2473,9 +2519,14 @@ dependencies = [ name = "prototypes" version = "0.1.0" dependencies = [ + "common", "egui-inspect", "geom", + "log", + "mlua", "serde", + "slotmapd", + "thiserror", ] [[package]] diff --git a/assets/items.json b/assets/items.json index 102b8158e..2eb646364 100644 --- a/assets/items.json +++ b/assets/items.json @@ -8,84 +8,5 @@ "name": "cereal", "label": "Cereal" }, - { - "name": "flour", - "label": "Flour" - }, - { - "name": "bread", - "label": "Bread" - }, - { - "name": "vegetable", - "label": "Vegetable" - }, - { - "name": "carcass", - "label": "Carcass" - }, - { - "name": "raw-meat", - "label": "Raw meat" - }, - { - "name": "meat", - "label": "Meat" - }, - { - "name": "tree-log", - "label": "Tree Log" - }, - { - "name": "wood-plank", - "label": "Wood Plank" - }, - { - "name": "iron-ore", - "label": "Iron Ore" - }, - { - "name": "metal", - "label": "Metal" - }, - { - "name": "gold", - "label": "Gold" - }, - { - "name": "high-tech-product", - "label": "High Tech Product" - }, - { - "name": "furniture", - "label": "Furniture" - }, - { - "name": "flower", - "label": "Flower" - }, - { - "name": "wool", - "label": "Wool" - }, - { - "name": "cloth", - "label": "Cloth" - }, - { - "name": "oil", - "label": "Oil" - }, - { - "name": "coal", - "label": "Coal" - }, - { - "name": "electricity", - "label": "Electricity" - }, - { - "name": "polyester", - "label": "Polyester" - } + ] \ No newline at end of file diff --git a/assets_gui/src/companies.rs b/assets_gui/src/companies.rs index 4088014b0..cb6b0ed9e 100644 --- a/assets_gui/src/companies.rs +++ b/assets_gui/src/companies.rs @@ -1,9 +1,9 @@ use common::saveload::Encoder; -use prototypes::GoodsCompanyDescriptionJSON; +use prototypes::GoodsCompanyPrototypeJSON; use std::io; pub struct Companies { - pub companies: Vec, + pub companies: Vec, pub changed: bool, } diff --git a/base/companies.lua b/base/companies.lua new file mode 100644 index 000000000..e69de29bb diff --git a/base/data.lua b/base/data.lua new file mode 100644 index 000000000..7869514d0 --- /dev/null +++ b/base/data.lua @@ -0,0 +1 @@ +require("items") \ No newline at end of file diff --git a/base/items.lua b/base/items.lua new file mode 100644 index 000000000..dcbdcd421 --- /dev/null +++ b/base/items.lua @@ -0,0 +1,113 @@ +data:extend { + { + type = "item", + name = "job-opening", + label = "Job opening", + optout_exttrade = true + }, + { + type = "item", + name = "cereal", + label = "Cereal" + }, + { + type = "item", + name = "flour", + label = "Flour" + }, + { + type = "item", + name = "bread", + label = "Bread" + }, + { + type = "item", + name = "vegetable", + label = "Vegetable" + }, + { + type = "item", + name = "carcass", + label = "Carcass" + }, + { + type = "item", + name = "raw-meat", + label = "Raw meat" + }, + { + type = "item", + name = "meat", + label = "Meat" + }, + { + type = "item", + name = "tree-log", + label = "Tree Log" + }, + { + type = "item", + name = "wood-plank", + label = "Wood Plank" + }, + { + type = "item", + name = "iron-ore", + label = "Iron Ore" + }, + { + type = "item", + name = "metal", + label = "Metal" + }, + { + type = "item", + name = "gold", + label = "Gold" + }, + { + type = "item", + name = "high-tech-product", + label = "High Tech Product" + }, + { + type = "item", + name = "furniture", + label = "Furniture" + }, + { + type = "item", + name = "flower", + label = "Flower" + }, + { + type = "item", + name = "wool", + label = "Wool" + }, + { + type = "item", + name = "cloth", + label = "Cloth" + }, + { + type = "item", + name = "oil", + label = "Oil" + }, + { + type = "item", + name = "coal", + label = "Coal" + }, + { + type = "item", + name = "electricity", + label = "Electricity" + }, + { + type = "item", + name = "polyester", + label = "Polyester" + } +} \ No newline at end of file diff --git a/common/src/lib.rs b/common/src/lib.rs index 98014af99..df91796db 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -231,6 +231,16 @@ pub fn fastset_with_capacity(cap: usize) -> FastSet { FastSet::with_capacity_and_hasher(cap, Default::default()) } +pub type TransparentMap = std::collections::HashMap; + +pub fn transparentmap_with_capacity(cap: usize) -> TransparentMap { + TransparentMap::with_capacity_and_hasher(cap, Default::default()) +} + +pub fn transparentmap() -> TransparentMap { + TransparentMap::with_hasher(Default::default()) +} + #[derive(Default)] pub struct TransparentHasherU64(u64); diff --git a/common/src/timestep.rs b/common/src/timestep.rs index 9da0cc938..e41f99921 100644 --- a/common/src/timestep.rs +++ b/common/src/timestep.rs @@ -4,7 +4,7 @@ pub const UP_DT: Duration = Duration::from_millis(20); /// A timestep that can be used to update the game state. /// It will try to keep a constant update rate. -/// Based on https://gafferongames.com/post/fix_your_timestep/ +/// Based on pub struct Timestep { last_time: Instant, acc: Duration, diff --git a/geom/Cargo.toml b/geom/Cargo.toml index 7e7cd9f1e..32fd1abfa 100644 --- a/geom/Cargo.toml +++ b/geom/Cargo.toml @@ -8,7 +8,8 @@ edition = "2021" [dependencies] ordered-float = { workspace = true } -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } fnv = "1.0.3" -inline_tweak = "1.0.8" -flat_spatial = { workspace = true } \ No newline at end of file +inline_tweak = "1.0.8" +flat_spatial = { workspace = true } +mlua = "0.9.4" \ No newline at end of file diff --git a/geom/src/aabb3.rs b/geom/src/aabb3.rs index 01121b74e..427694805 100644 --- a/geom/src/aabb3.rs +++ b/geom/src/aabb3.rs @@ -137,7 +137,7 @@ impl AABB3 { /// as ray is defined by O + tD, return the t values for the entering and exiting intersections /// Returns a 2-tuple of (t_near, t_far) - /// Adapted from https://gist.github.com/DomNomNom/46bb1ce47f68d255fd5d + /// Adapted from /// If the ray origin is inside the box, t_near will be zero #[inline] pub fn raycast(&self, ray: Ray3) -> Option<(f32, f32)> { diff --git a/geom/src/v2.rs b/geom/src/v2.rs index 7c7740beb..6c056a347 100644 --- a/geom/src/v2.rs +++ b/geom/src/v2.rs @@ -1,4 +1,5 @@ use crate::{Circle, Intersect, Polygon, Radians, Shape, Vec3, Vec3d, AABB, OBB}; +use mlua::{FromLua, Value}; use serde::{Deserialize, Deserializer, Serialize}; use std::fmt::{Debug, Display, Formatter}; use std::hash::{Hash, Hasher}; @@ -1170,6 +1171,29 @@ impl From<[f64; 2]> for Vec2d { } } +impl<'a> FromLua<'a> for Vec2 { + fn from_lua(value: Value<'a>, _: &'a mlua::Lua) -> mlua::Result { + let t = match value { + Value::Vector(v) => return Ok(Self { x: v.x(), y: v.y() }), + Value::Table(t) => t, + _ => { + return Err(mlua::Error::FromLuaConversionError { + from: value.type_name(), + to: "Vec2", + message: Some("expected a table or vector".to_string()), + }) + } + }; + if let Ok(x) = t.get(1) { + return Ok(Self { x, y: t.get(2)? }); + } + + let x = t.get("x")?; + let y = t.get("y")?; + Ok(Vec2::new(x, y)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/geom/src/v3.rs b/geom/src/v3.rs index 2dc90ed3b..ac2a3e3e8 100644 --- a/geom/src/v3.rs +++ b/geom/src/v3.rs @@ -1,4 +1,5 @@ use crate::{vec2, vec4, Shape3, Vec2, Vec4, AABB3}; +use mlua::{FromLua, Value}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{Debug, Display, Formatter}; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; @@ -636,3 +637,37 @@ impl Vec3 { m < f32::EPSILON } } + +impl<'a> FromLua<'a> for Vec3 { + fn from_lua(value: Value<'a>, _: &'a mlua::Lua) -> mlua::Result { + let t = match value { + Value::Vector(v) => { + return Ok(Self { + x: v.x(), + y: v.y(), + z: v.z(), + }) + } + Value::Table(t) => t, + _ => { + return Err(mlua::Error::FromLuaConversionError { + from: value.type_name(), + to: "Vec3", + message: Some("expected a table or vector".to_string()), + }) + } + }; + if let Ok(x) = t.get(1) { + return Ok(Self { + x, + y: t.get(2)?, + z: t.get(3)?, + }); + } + + let x = t.get("x")?; + let y = t.get("y")?; + let z = t.get("z")?; + Ok(Self { x, y, z }) + } +} diff --git a/native_app/src/gui/inspect/inspect_building.rs b/native_app/src/gui/inspect/inspect_building.rs index dd12c4a4e..5abeece2b 100644 --- a/native_app/src/gui/inspect/inspect_building.rs +++ b/native_app/src/gui/inspect/inspect_building.rs @@ -10,7 +10,7 @@ use egui_inspect::{Inspect, InspectArgs, InspectVec2Rotation}; use simulation::map::{Building, BuildingID, BuildingKind, Zone, MAX_ZONE_AREA}; use simulation::map_dynamic::BuildingInfos; use simulation::souls::freight_station::FreightTrainState; -use simulation::souls::goods_company::{GoodsCompanyRegistry, Recipe}; +use simulation::souls::goods_company::{GoodsCompanyRegistry, RecipePrototype}; /// Inspect a specific building, showing useful information about it pub fn inspect_building(uiworld: &mut UiWorld, sim: &Simulation, ui: &Context, id: BuildingID) { @@ -186,7 +186,7 @@ fn render_goodscompany(ui: &mut Ui, uiworld: &mut UiWorld, sim: &Simulation, b: } } -fn render_recipe(ui: &mut Ui, uiworld: &UiWorld, sim: &Simulation, recipe: &Recipe) { +fn render_recipe(ui: &mut Ui, uiworld: &UiWorld, sim: &Simulation, recipe: &RecipePrototype) { let registry = sim.read::(); if recipe.consumption.is_empty() { diff --git a/prototypes/Cargo.toml b/prototypes/Cargo.toml index f980d4ac1..e666bb2a4 100644 --- a/prototypes/Cargo.toml +++ b/prototypes/Cargo.toml @@ -6,6 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = "1.0.195" -geom = { path = "../geom" } -egui-inspect = { path = "../egui-inspect" } \ No newline at end of file +common = { path = "../common" } +geom = { path = "../geom" } +egui-inspect = { path = "../egui-inspect" } +mlua = { version = "0.9.4", features = ["luau"] } +slotmapd = "1.0.10" +serde = "1.0.195" +thiserror = "1.0.56" +log = { version = "0.4.20", features = [] } \ No newline at end of file diff --git a/prototypes/src/company.rs b/prototypes/src/company.rs new file mode 100644 index 000000000..348fb8b7d --- /dev/null +++ b/prototypes/src/company.rs @@ -0,0 +1,204 @@ +use crate::{GoodsCompanyID, ItemID, Prototype, PrototypeBase, RecipeID}; +use egui_inspect::{debug_inspect_impl, Inspect}; +use geom::Vec2; +use mlua::{FromLua, Lua, Table, Value}; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; + +#[derive(Debug, Clone, Inspect)] +pub struct RecipePrototype { + pub base: PrototypeBase, + pub consumption: Vec<(ItemID, i32)>, + pub production: Vec<(ItemID, i32)>, + + /// Time to execute the recipe when the facility is at full capacity, in seconds + pub complexity: i32, + + /// Quantity to store per production in terms of quantity produced. So if it takes 1ton of flour to make + /// 1 ton of bread. A storage multiplier of 3 means 3 tons of bread will be stored before stopping to + /// produce it. + pub storage_multiplier: i32, +} + +impl Deref for RecipePrototype { + type Target = PrototypeBase; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl Prototype for RecipePrototype { + type ID = RecipeID; + const KIND: &'static str = "recipe"; + + fn from_lua(table: &Table) -> mlua::Result { + let consumption: Table = table.get("consumption")?; + let production: Table = table.get("production")?; + + let mut consumption_v = Vec::with_capacity(consumption.raw_len()); + consumption.for_each(|_: usize, v: Table| { + let name = v.get::<_, String>("name")?; + let item_id = ItemID::from(&name); + let amount = v.get::<_, i32>("amount")?; + consumption_v.push((item_id, amount)); + Ok(()) + })?; + + let mut production_v = Vec::with_capacity(production.raw_len()); + production.for_each(|_: usize, v: Table| { + let name = v.get::<_, String>("name")?; + let item_id = ItemID::from(&name); + let amount = v.get::<_, i32>("amount")?; + production_v.push((item_id, amount)); + Ok(()) + })?; + + Ok(Self { + base: PrototypeBase::from_lua(table)?, + consumption: consumption_v, + production: production_v, + complexity: table.get("complexity")?, + storage_multiplier: table.get("storage_multiplier")?, + }) + } + + fn id(&self) -> Self::ID { + Self::ID::from(&self.name) + } +} + +#[derive(Debug)] +pub struct GoodsCompanyPrototype { + pub base: PrototypeBase, + pub id: GoodsCompanyID, + pub label: String, + pub bgen: BuildingGen, + pub kind: CompanyKind, + pub recipe: RecipeID, + pub n_workers: i32, + pub size: f32, + pub asset_location: String, + pub price: i64, + pub zone: Option, +} + +impl Prototype for GoodsCompanyPrototype { + type ID = GoodsCompanyID; + const KIND: &'static str = "goods-company"; + + fn from_lua(table: &Table) -> mlua::Result { + Ok(Self { + base: PrototypeBase::from_lua(table)?, + id: GoodsCompanyID::from(&table.get::<_, String>("name")?), + label: table.get("label")?, + bgen: table.get("bgen")?, + kind: table.get("kind")?, + recipe: table.get("recipe")?, + n_workers: table.get("n_workers")?, + size: table.get("size")?, + asset_location: table.get("asset_location")?, + price: table.get("price")?, + zone: table.get("zone").ok(), + }) + } + + fn id(&self) -> Self::ID { + self.id + } +} + +impl Deref for GoodsCompanyPrototype { + type Target = PrototypeBase; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +#[derive(Copy, Clone, Serialize, Deserialize, Debug)] +pub enum CompanyKind { + // Buyers come to get their goods + Store, + // Buyers get their goods delivered to them + Factory { n_trucks: u32 }, + // Buyers get their goods instantly delivered, useful for things like electricity/water/.. + Network, +} + +impl<'a> FromLua<'a> for CompanyKind { + fn from_lua(value: Value<'a>, lua: &'a Lua) -> mlua::Result { + let table: Table = FromLua::from_lua(value, lua)?; + let kind = table.get::<_, String>("type")?; + match kind.as_str() { + "store" => Ok(Self::Store), + "factory" => Ok(Self::Factory { + n_trucks: table.get("n_trucks")?, + }), + "network" => Ok(Self::Network), + _ => Err(mlua::Error::external(format!( + "Unknown company kind: {}", + kind + ))), + } + } +} + +debug_inspect_impl!(CompanyKind); + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub enum BuildingGen { + House, + Farm, + CenteredDoor { + vertical_factor: f32, // 1.0 means that the door is at the bottom, just on the street + }, + NoWalkway { + door_pos: Vec2, // door_pos is relative to the center of the building + }, +} + +debug_inspect_impl!(BuildingGen); + +impl<'a> FromLua<'a> for BuildingGen { + fn from_lua(value: Value<'a>, lua: &'a Lua) -> mlua::Result { + let table: Table = FromLua::from_lua(value, lua)?; + let kind = table.get::<_, String>("kind")?; + match kind.as_str() { + "house" => Ok(Self::House), + "farm" => Ok(Self::Farm), + "centered_door" => Ok(Self::CenteredDoor { + vertical_factor: table.get("vertical_factor")?, + }), + "no_walkway" => Ok(Self::NoWalkway { + door_pos: table.get("door_pos")?, + }), + _ => Err(mlua::Error::external(format!( + "Unknown building gen kind: {}", + kind + ))), + } + } +} + +#[derive(Debug)] +pub struct ZoneDescription { + pub floor: String, + pub filler: String, + /// The price for each "production unit" + pub price_per_area: i64, + /// Whether the zone filler positions should be randomized + pub randomize_filler: bool, +} + +impl<'lua> FromLua<'lua> for ZoneDescription { + fn from_lua(value: Value<'lua>, lua: &'lua Lua) -> mlua::Result { + let table: Table = FromLua::from_lua(value, lua)?; + Ok(Self { + floor: table.get("floor")?, + filler: table.get("filler")?, + price_per_area: table.get("price_per_area").unwrap_or(100), + randomize_filler: table.get("randomize_filler").unwrap_or(false), + }) + } +} diff --git a/prototypes/src/item.rs b/prototypes/src/item.rs new file mode 100644 index 000000000..0398c4ccc --- /dev/null +++ b/prototypes/src/item.rs @@ -0,0 +1,39 @@ +use crate::{ItemID, Prototype, PrototypeBase}; +use mlua::Table; +use std::ops::Deref; + +/// Item is the runtime representation of an item, such as meat, wood, etc. +#[derive(Clone, Debug)] +pub struct ItemPrototype { + pub id: ItemID, + pub base: PrototypeBase, + pub label: String, + pub optout_exttrade: bool, +} + +impl Deref for ItemPrototype { + type Target = PrototypeBase; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl Prototype for ItemPrototype { + type ID = ItemID; + const KIND: &'static str = "item"; + + fn from_lua(table: &Table) -> mlua::Result { + let base = PrototypeBase::from_lua(table)?; + Ok(Self { + id: ItemID::new(&base.name), + base, + label: table.get("label")?, + optout_exttrade: table.get("optout_exttrade").unwrap_or(false), + }) + } + + fn id(&self) -> Self::ID { + ItemID::from(&self.name) + } +} diff --git a/prototypes/src/lib.rs b/prototypes/src/lib.rs index c74dc6552..6a7171887 100644 --- a/prototypes/src/lib.rs +++ b/prototypes/src/lib.rs @@ -1,74 +1,257 @@ -use egui_inspect::debug_inspect_impl; -use geom::Vec2; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -pub struct RecipeDescription { - pub consumption: Vec<(String, i32)>, - pub production: Vec<(String, i32)>, - pub complexity: i32, - pub storage_multiplier: i32, +use common::TransparentMap; +use egui_inspect::Inspect; +use mlua::{Lua, Table, Value}; +use std::hash::Hash; +use std::io; +use thiserror::Error; + +mod company; +mod item; + +pub use company::*; +pub use item::*; + +pub trait Prototype: 'static + Sized { + type ID: Copy + Clone + Eq + Ord + Hash + 'static; + const KIND: &'static str; + + fn from_lua(table: &Table) -> mlua::Result; + fn id(&self) -> Self::ID; } -#[derive(Copy, Clone, Serialize, Deserialize, Debug)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum CompanyKind { - // Buyers come to get their goods - Store, - // Buyers get their goods delivered to them - Factory { n_trucks: u32 }, - // Buyers get their goods instantly delivered, useful for things like electricity/water/.. - Network, +pub trait ConcretePrototype: Prototype { + fn storage(prototypes: &Prototypes) -> &TransparentMap; + fn storage_mut(prototypes: &mut Prototypes) -> &mut TransparentMap; } -debug_inspect_impl!(CompanyKind); - -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum BuildingGen { - House, - Farm, - CenteredDoor { - vertical_factor: f32, // 1.0 means that the door is at the bottom, just on the street - }, - NoWalkway { - door_pos: Vec2, // door_pos is relative to the center of the building - }, +pub trait PrototypeID: Copy + Clone + Eq + Ord + Hash + 'static { + type Prototype: Prototype; } -#[derive(Serialize, Deserialize)] -pub struct GoodsCompanyDescriptionJSON { +#[derive(Debug, Clone, Inspect)] +pub struct PrototypeBase { pub name: String, - pub bgen: BuildingGen, - #[serde(flatten)] - pub kind: CompanyKind, - pub recipe: RecipeDescription, - pub n_workers: i32, - pub size: f32, - pub asset_location: String, - pub price: i64, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub zone: Option>, } -#[derive(Debug, Serialize, Deserialize)] -pub struct ZoneDescription { - pub floor: String, - pub filler: String, - /// The price for each "production unit" - pub price_per_area: i64, - /// Wether the zone filler positions should be randomized - #[serde(default)] - pub randomize_filler: bool, +impl Prototype for PrototypeBase { + type ID = (); + const KIND: &'static str = "base"; + + fn from_lua(table: &Table) -> mlua::Result { + Ok(Self { + name: table.get("name")?, + }) + } + + fn id(&self) -> Self::ID { + () + } +} + +static mut PROTOTYPES: Option<&'static Prototypes> = None; + +#[inline] +pub fn prototypes() -> &'static Prototypes { + #[cfg(debug_assertions)] + { + assert!(unsafe { PROTOTYPES.is_some() }); + } + + // Safety: Please just don't use prototypes before they were loaded... We can allow this footgun + unsafe { PROTOTYPES.unwrap_unchecked() } +} + +pub fn prototype(id: ID) -> Option<&'static ::Prototype> +where + ID::Prototype: ConcretePrototype, +{ + ::Prototype::storage(prototypes()).get(&id) +} + +pub fn prototypes_iter() -> impl Iterator { + T::storage(prototypes()).values() +} + +#[derive(Error, Debug)] +pub enum PrototypeLoadError { + #[error("loading data.lua: {0}")] + LoadingDataLua(#[from] io::Error), + #[error("lua error: {0}")] + LuaError(#[from] mlua::Error), + #[error("multiple errors: {0:?}")] + MultiError(Vec), +} + +macro_rules! prototype_id { + ($t:ident => $proto:ty) => { + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub struct $t(pub u64); + + egui_inspect::debug_inspect_impl!($t); + + impl $t { + pub fn new(v: &str) -> $t { + Self(common::hash_u64(v)) + } + } + + impl<'a> From<&'a str> for $t { + fn from(v: &'a str) -> Self { + Self(common::hash_u64(v)) + } + } + + impl<'a> From<&'a String> for $t { + fn from(v: &'a String) -> Self { + Self(common::hash_u64(&*v)) + } + } + + impl<'a> mlua::FromLua<'a> for $t { + fn from_lua(v: mlua::Value<'a>, _: &'a mlua::Lua) -> mlua::Result { + match v { + mlua::Value::String(s) => { + let Ok(v) = s.to_str() else { + return Err(mlua::Error::FromLuaConversionError { + from: "string", + to: stringify!($t), + message: Some("expected utf-8 string".into()), + }); + }; + Ok(Self(common::hash_u64(v))) + } + _ => Err(mlua::Error::FromLuaConversionError { + from: v.type_name(), + to: stringify!($t), + message: Some("expected string".into()), + }), + } + } + } + + impl crate::PrototypeID for $t { + type Prototype = $proto; + } + }; +} + +macro_rules! gen_prototypes { + ($($name:ident : $id:ident => $t:ty,)+) => { + + pub struct Prototypes { + $( + $name: TransparentMap<$id, $t>, + )+ + } + + $( + impl ConcretePrototype for $t { + fn storage(prototypes: &Prototypes) -> &TransparentMap { + &prototypes.$name + } + + fn storage_mut(prototypes: &mut Prototypes) -> &mut TransparentMap { + &mut prototypes.$name + } + } + )+ + + fn parse_prototype(table: Table, proto: &mut Prototypes) -> Result<(), PrototypeLoadError> { + match table.get::<_, String>("type")?.as_str() { + $( + <$t>::KIND => { + let p: $t = Prototype::from_lua(&table)?; + if let Some(v) = proto.$name.insert((&p.name).into(), p) { + log::warn!("duplicate {} with name: {}", <$t>::KIND, v.name); + } + } + ),+ + _ => { + if let Ok(s) = table.get::<_, String>("type") { + log::warn!("unknown prototype: {}", s) + } + }, + } + Ok(()) + } + + $( + prototype_id!($id => $t); + )+ + }; +} + +gen_prototypes!(companies: GoodsCompanyID => GoodsCompanyPrototype, + recipes: RecipeID => RecipePrototype, + items: ItemID => ItemPrototype, +); + +/// Loads the prototypes from the data.lua file +/// # Safety +/// This function is not thread safe, and should only be called once at the start of the program. +pub unsafe fn load_prototypes(base: &str) -> Result<(), PrototypeLoadError> { + let l = Lua::new(); + + let base = base.to_string(); + + l.globals() + .get::<_, Table>("package")? + .set("path", base.clone() + "base/?.lua")?; + + l.load(include_str!("prototype_init.lua")).exec()?; + + l.load(common::saveload::load_raw(base + "base/data.lua")?) + .exec()?; + + let mut p = Box::new(Prototypes { + recipes: Default::default(), + companies: Default::default(), + items: Default::default(), + }); + + let mut errors = Vec::new(); + + let _ = data_table.for_each(|_: String, t: Table| { + let r = parse_prototype(t, &mut *p); + if let Err(e) = r { + errors.push(e); + } + Ok(()) + }); + + if !errors.is_empty() { + return Err(PrototypeLoadError::MultiError(errors)); + } + + unsafe { + PROTOTYPES = Some(Box::leak(p)); + } + + Ok(()) } -impl Default for ZoneDescription { - fn default() -> Self { - Self { - floor: "".to_string(), - filler: "".to_string(), - price_per_area: 100, - randomize_filler: false, +#[cfg(test)] +mod tests { + use crate::{load_prototypes, prototype, ItemID}; + + #[test] + fn test_base() { + unsafe { + match load_prototypes("../") { + Ok(_) => {} + Err(e) => { + println!("failed to load prototypes: {}", e); + assert!(false); + } + } + + println!( + "{:?}", + prototype(ItemID::new("job-opening")) + .unwrap() + .optout_exttrade + ); + println!("{:?}", prototype(ItemID::new("cereal"))); } } } diff --git a/prototypes/src/prototype_init.lua b/prototypes/src/prototype_init.lua new file mode 100644 index 000000000..72072c728 --- /dev/null +++ b/prototypes/src/prototype_init.lua @@ -0,0 +1,12 @@ +data = {} + +function data:extend (t) + for _, v in ipairs(t) do + rawset(self, rawlen(self)+1, v) + end +end + +setmetatable(data, { + __index = data, + __newindex = function (t, k, v) end, +}) \ No newline at end of file diff --git a/simulation/src/economy/item.rs b/simulation/src/economy/item.rs index 1fbc252c3..8b1378917 100644 --- a/simulation/src/economy/item.rs +++ b/simulation/src/economy/item.rs @@ -1,86 +1 @@ -use common::saveload::Encoder; -use common::FastMap; -use serde::{Deserialize, Serialize}; -use slotmapd::{new_key_type, SlotMap}; -use std::ops::Index; -/// ItemDefinition is the definition of an item, as read from the items.json file. -#[derive(Serialize, Deserialize)] -struct ItemDefinition { - name: String, - label: String, - #[serde(default)] - optout_exttrade: bool, -} - -/// Item is the runtime representation of an item, such as meat, wood, etc. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Item { - pub id: ItemID, - pub name: String, - pub label: String, - pub optout_exttrade: bool, -} - -new_key_type! { - pub struct ItemID; -} - -debug_inspect_impl!(ItemID); - -#[derive(Default, Serialize, Deserialize)] -pub struct ItemRegistry { - items: SlotMap, - item_names: FastMap, -} - -impl Index for ItemRegistry { - type Output = Item; - fn index(&self, index: ItemID) -> &Self::Output { - &self.items[index] - } -} - -impl ItemRegistry { - pub fn id(&self, name: &str) -> ItemID { - self.item_names - .get(name) - .copied() - .unwrap_or_else(|| panic!("no item in registry named {name}")) - } - - pub fn try_id(&self, name: &str) -> Option { - self.item_names.get(name).copied() - } - - pub fn get(&self, id: ItemID) -> Option<&Item> { - self.items.get(id) - } - - pub fn iter(&self) -> impl Iterator + '_ { - self.items.values() - } - - pub fn load_item_definitions(&mut self, source: &str) { - let definitions: Vec = match common::saveload::JSON::decode(source.as_ref()) - { - Ok(x) => x, - Err(e) => { - log::error!("error loading item definitions: {}", e); - return; - } - }; - for definition in definitions { - let name = definition.name.clone(); - let id = self.items.insert_with_key(move |id| Item { - id, - name: definition.name, - label: definition.label, - optout_exttrade: definition.optout_exttrade, - }); - self.item_names.insert(name, id); - #[cfg(not(test))] - log::debug!("loaded {:?}", &self.items[id]); - } - } -} diff --git a/simulation/src/economy/market.rs b/simulation/src/economy/market.rs index a5a80c7a8..ee30d8880 100644 --- a/simulation/src/economy/market.rs +++ b/simulation/src/economy/market.rs @@ -5,6 +5,7 @@ use crate::souls::goods_company::GoodsCompanyID; use crate::{BuildingKind, GoodsCompanyRegistry, Map, SoulID}; use geom::Vec2; use ordered_float::OrderedFloat; +use prototypes::{prototypes_iter, ItemPrototype}; use serde::{Deserialize, Serialize}; use std::collections::btree_map::Entry; use std::collections::BTreeMap; @@ -121,11 +122,10 @@ pub fn find_trade_place( } impl Market { - pub fn new(registry: &ItemRegistry, companies: &GoodsCompanyRegistry) -> Self { - let prices = calculate_prices(registry, companies, 1.25); + pub fn new() -> Self { + let prices = calculate_prices(1.25); Self { - markets: registry - .iter() + markets: prototypes_iter::() .map(|v| (v.id, SingleMarket::new(prices[&v.id], v.optout_exttrade))) .collect(), all_trades: Default::default(), @@ -360,11 +360,7 @@ impl Market { } } -fn calculate_prices( - registry: &ItemRegistry, - companies: &GoodsCompanyRegistry, - price_multiplier: f32, -) -> BTreeMap { +fn calculate_prices(price_multiplier: f32) -> BTreeMap { let mut item_graph: BTreeMap> = BTreeMap::new(); for (id, company) in companies.descriptions.iter() { for (itemid, _) in &company.recipe.production { @@ -439,7 +435,7 @@ fn calculate_prices( mod tests { use super::Market; use crate::economy::{ItemRegistry, WORKER_CONSUMPTION_PER_SECOND}; - use crate::souls::goods_company::{GoodsCompanyDescription, Recipe}; + use crate::souls::goods_company::{GoodsCompanyPrototype, RecipePrototype}; use crate::world::CompanyID; use crate::{GoodsCompanyRegistry, SoulID}; use common::descriptions::{BuildingGen, CompanyKind}; @@ -516,12 +512,12 @@ mod tests { companies .descriptions - .insert_with_key(|id| GoodsCompanyDescription { + .insert_with_key(|id| GoodsCompanyPrototype { id, name: "Cereal farm".to_string(), bgen: BuildingGen::House, kind: CompanyKind::Store, - recipe: Recipe { + recipe: RecipePrototype { production: vec![(cereal, 3)], complexity: 3, consumption: vec![], @@ -536,12 +532,12 @@ mod tests { companies .descriptions - .insert_with_key(|id| GoodsCompanyDescription { + .insert_with_key(|id| GoodsCompanyPrototype { id, name: "Wheat factory".to_string(), bgen: BuildingGen::House, kind: CompanyKind::Store, - recipe: Recipe { + recipe: RecipePrototype { production: vec![(wheat, 2)], complexity: 10, consumption: vec![(cereal, 2)], diff --git a/simulation/src/economy/mod.rs b/simulation/src/economy/mod.rs index 39c14bf76..d79bcdb0b 100644 --- a/simulation/src/economy/mod.rs +++ b/simulation/src/economy/mod.rs @@ -28,6 +28,7 @@ pub use ecostats::*; pub use government::*; pub use item::*; pub use market::*; +use prototypes::ItemID; const WORKER_CONSUMPTION_PER_SECOND: Money = Money::new_cents(1); @@ -164,31 +165,10 @@ pub struct Bought(pub BTreeMap>); #[derive(Inspect, Debug, Default, Serialize, Deserialize)] pub struct Workers(pub Vec); -#[cfg(not(test))] -const ITEMS_PATH: &str = "assets/items.json"; -#[cfg(not(test))] -const COMPANIES_PATH: &str = "assets/companies.json"; - -#[cfg(test)] -const ITEMS_PATH: &str = "../assets/items.json"; -#[cfg(test)] -const COMPANIES_PATH: &str = "../assets/companies.json"; - pub fn init_market(_: &mut World, res: &mut Resources) { - res.write::() - .load_item_definitions(&common::saveload::load_string(ITEMS_PATH).unwrap()); - - res.write::().load( - &common::saveload::load_string(COMPANIES_PATH).unwrap(), - &res.read::(), - ); - - let market = Market::new( - &res.read::(), - &res.read::(), - ); + let market = Market::new(); res.insert(market); - let stats = EcoStats::new(&res.read::()); + let stats = EcoStats::new(); res.insert(stats); } diff --git a/simulation/src/souls/goods_company.rs b/simulation/src/souls/goods_company.rs index 85e705207..59eddb7aa 100644 --- a/simulation/src/souls/goods_company.rs +++ b/simulation/src/souls/goods_company.rs @@ -1,5 +1,10 @@ -use super::desire::Work; -use crate::economy::{find_trade_place, ItemID, ItemRegistry, Market}; +use serde::{Deserialize, Serialize}; + +use egui_inspect::Inspect; +use geom::{Transform, Vec2}; +use prototypes::{CompanyKind, RecipePrototype}; + +use crate::economy::{find_trade_place, Market}; use crate::map::{Building, BuildingID, Map, Zone, MAX_ZONE_AREA}; use crate::map_dynamic::BuildingInfos; use crate::souls::desire::WorkKind; @@ -8,147 +13,45 @@ use crate::utils::time::GameTime; use crate::world::{CompanyEnt, HumanEnt, HumanID, VehicleID}; use crate::{ParCommandBuffer, SoulID}; use crate::{Simulation, World}; -use common::saveload::Encoder; -use egui_inspect::Inspect; -use geom::{Transform, Vec2}; -use prototypes::{BuildingGen, CompanyKind, GoodsCompanyDescriptionJSON, ZoneDescription}; -use serde::{Deserialize, Serialize}; -use slotmapd::{new_key_type, SlotMap}; - -#[derive(Debug, Clone, Serialize, Deserialize, Inspect)] -pub struct Recipe { - pub consumption: Vec<(ItemID, i32)>, - pub production: Vec<(ItemID, i32)>, - - /// Time to execute the recipe when the facility is at full capacity, in seconds - pub complexity: i32, - - /// Quantity to store per production in terms of quantity produced. So if it takes 1ton of flour to make - /// 1 ton of bread. A storage multiplier of 3 means 3 tons of bread will be stored before stopping to - /// produce it. - pub storage_multiplier: i32, -} - -new_key_type! { - pub struct GoodsCompanyID; -} - -debug_inspect_impl!(GoodsCompanyID); - -#[derive(Debug)] -pub struct GoodsCompanyDescription { - pub id: GoodsCompanyID, - pub name: String, - pub bgen: BuildingGen, - pub kind: CompanyKind, - pub recipe: Recipe, - pub n_workers: i32, - pub size: f32, - pub asset_location: String, - pub price: i64, - pub zone: Option>, -} - -#[derive(Default)] -pub struct GoodsCompanyRegistry { - pub descriptions: SlotMap, -} - -impl GoodsCompanyRegistry { - pub fn load(&mut self, source: &str, registry: &ItemRegistry) { - let descriptions: Vec = - match common::saveload::JSON::decode(source.as_ref()) { - Ok(x) => x, - Err(e) => { - log::error!("couldn't load goods company descriptions: {}", e); - return; - } - }; - for descr in descriptions { - let recipe = Recipe { - consumption: descr - .recipe - .consumption - .into_iter() - .map(|(item, qty)| { - let item_id = registry.id(&item); - (item_id, qty) - }) - .collect(), - production: descr - .recipe - .production - .into_iter() - .map(|(item, qty)| { - let item_id = registry.id(&item); - (item_id, qty) - }) - .collect(), - complexity: descr.recipe.complexity, - storage_multiplier: descr.recipe.storage_multiplier, - }; - - #[allow(unused_variables)] - let id = self - .descriptions - .insert_with_key(move |id| GoodsCompanyDescription { - id, - name: descr.name, - bgen: descr.bgen, - kind: descr.kind, - recipe, - n_workers: descr.n_workers, - size: descr.size, - asset_location: descr.asset_location, - price: descr.price, - zone: descr.zone, - }); +use super::desire::Work; - #[cfg(not(test))] - log::debug!("loaded {:?}", &self.descriptions[id]); - } +pub fn recipe_init(recipe: &RecipePrototype, soul: SoulID, near: Vec2, market: &mut Market) { + for &(kind, qty) in &recipe.consumption { + market.buy_until(soul, near, kind, qty as u32) } -} - -impl Recipe { - pub fn init(&self, soul: SoulID, near: Vec2, market: &mut Market) { - for &(kind, qty) in &self.consumption { - market.buy_until(soul, near, kind, qty as u32) - } - for &(kind, _) in &self.production { - market.register(soul, kind); - } + for &(kind, _) in &recipe.production { + market.register(soul, kind); } +} - pub fn should_produce(&self, soul: SoulID, market: &Market) -> bool { - // Has enough resources - self.consumption +pub fn recipe_should_produce(recipe: &RecipePrototype, soul: SoulID, market: &Market) -> bool { + // Has enough resources + recipe.consumption .iter() .all(move |&(kind, qty)| market.capital(soul, kind) >= qty) && // Has enough storage - self.production.iter().all(move |&(kind, qty)| { - market.capital(soul, kind) < qty * (self.storage_multiplier + 1) + recipe.production.iter().all(move |&(kind, qty)| { + market.capital(soul, kind) < qty * (recipe.storage_multiplier + 1) }) - } +} - pub fn act(&self, soul: SoulID, near: Vec2, market: &mut Market) { - for &(kind, qty) in &self.consumption { - market.produce(soul, kind, -qty); - market.buy_until(soul, near, kind, qty as u32); - } - for &(kind, qty) in &self.production { - market.produce(soul, kind, qty); - market.sell_all(soul, near, kind, (qty * self.storage_multiplier) as u32); - } +pub fn recipe_act(recipe: &RecipePrototype, soul: SoulID, near: Vec2, market: &mut Market) { + for &(kind, qty) in &recipe.consumption { + market.produce(soul, kind, -qty); + market.buy_until(soul, near, kind, qty as u32); + } + for &(kind, qty) in &recipe.production { + market.produce(soul, kind, qty); + market.sell_all(soul, near, kind, (qty * recipe.storage_multiplier) as u32); } } #[derive(Clone, Serialize, Deserialize, Inspect)] pub struct GoodsCompany { pub kind: CompanyKind, - pub recipe: Recipe, + pub recipe: RecipePrototype, pub building: BuildingID, pub max_workers: i32, /// In [0; 1] range, to show how much has been made until new product