From bc78f3dfca8fc9bf0a885efa16eca0977ab24d7a Mon Sep 17 00:00:00 2001 From: Paris DOUADY Date: Wed, 17 Jan 2024 17:40:15 +0100 Subject: [PATCH] electricity affect productivity and show info about that in UI --- base_mod/companies.lua | 37 +++++-- .../src/gui/inspect/inspect_building.rs | 55 +++++++++-- native_app/src/gui/topgui.rs | 46 ++++++--- prototypes/src/lib.rs | 2 +- prototypes/src/macros.rs | 1 + prototypes/src/prototypes/company.rs | 21 ++-- prototypes/src/prototypes/solar.rs | 5 +- prototypes/src/types/power.rs | 98 ++++++++++++++++++- prototypes/src/validation.rs | 55 +++++++---- simulation/src/economy/market.rs | 18 ++-- simulation/src/init.rs | 6 +- .../{electricity.rs => electricity_cache.rs} | 31 +++--- simulation/src/map/height_override.rs | 4 +- simulation/src/map/map.rs | 10 +- simulation/src/map/mod.rs | 4 +- simulation/src/map/objects/building.rs | 13 +-- simulation/src/map/objects/intersection.rs | 28 ++++-- simulation/src/map/objects/lot.rs | 6 +- simulation/src/map/objects/road.rs | 2 +- simulation/src/map/serializing.rs | 22 ++--- simulation/src/map/spatial_map.rs | 65 ++++++++++-- simulation/src/map_dynamic/electricity.rs | 93 ++++++++++++++++++ simulation/src/map_dynamic/mod.rs | 2 + simulation/src/souls/goods_company.rs | 87 +++++++++++----- 24 files changed, 542 insertions(+), 169 deletions(-) rename simulation/src/map/{electricity.rs => electricity_cache.rs} (95%) create mode 100644 simulation/src/map_dynamic/electricity.rs diff --git a/base_mod/companies.lua b/base_mod/companies.lua index 3f74ef0f..103f5b99 100644 --- a/base_mod/companies.lua +++ b/base_mod/companies.lua @@ -19,6 +19,7 @@ data:extend { size = 10.0, asset_location = "bakery.glb", price = 1000, + power_consumption = "200W", }, { type = "goods-company", @@ -41,6 +42,7 @@ data:extend { size = 80.0, asset_location = "flour_factory.glb", price = 1000, + power_consumption = "10kW", }, { type = "goods-company", @@ -66,6 +68,7 @@ data:extend { price_per_area = 100, randomize_filler = true, }, + power_consumption = "100W", }, { type = "solar-panel", @@ -77,13 +80,7 @@ data:extend { kind = "centered_door", vertical_factor = 1.0, }, - kind = "network", - recipe = { - consumption = {}, - production = {}, - complexity = 1, - storage_multiplier = 5, - }, + kind = "factory", n_workers = 0, size = 120.0, asset_location = "assets/sprites/cement.jpg", @@ -93,6 +90,7 @@ data:extend { filler = "solarpanel.glb", price_per_area = 10, }, + power_production = "10kW", }, { type = "goods-company", @@ -103,18 +101,18 @@ data:extend { kind = "centered_door", vertical_factor = 1.0, }, - kind = "network", + kind = "factory", recipe = { consumption = {{"coal", 1}}, production = {}, complexity = 100, storage_multiplier = 5, - power_generation = "2.46MW", }, n_workers = 10, size = 165.0, asset_location = "coal_power_plant.glb", price = 1000, + power_production = "2.46MW", }, { type = "goods-company", @@ -136,6 +134,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/supermarket.png", price = 1000, + power_consumption = "1kW", }, { type = "goods-company", @@ -157,6 +156,7 @@ data:extend { size = 10.0, asset_location = "assets/sprites/clothes_store.png", price = 1000, + power_consumption = "1kW", }, { type = "goods-company", @@ -179,6 +179,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/cloth_factory.png", price = 1000, + power_consumption = "10kW", }, { type = "goods-company", @@ -201,6 +202,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/textile_processing_facility.png", price = 1000, + power_consumption = "1kW", }, { type = "goods-company", @@ -223,6 +225,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/polyester_refinery.png", price = 1000, + power_consumption = "10kW", }, { type = "goods-company", @@ -245,6 +248,7 @@ data:extend { size = 20.0, asset_location = "assets/sprites/oil_pump.png", price = 1000, + power_consumption = "10kW", }, { type = "goods-company", @@ -289,6 +293,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/wool_farm.png", price = 1000, + power_consumption = "100W", }, { type = "goods-company", @@ -310,6 +315,7 @@ data:extend { size = 10.0, asset_location = "assets/sprites/florist.png", price = 1000, + power_consumption = "100W", }, { type = "goods-company", @@ -332,6 +338,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/horticulturalist.png", price = 1000, + power_consumption = "500W", }, { type = "goods-company", @@ -353,6 +360,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/hightech_store.png", price = 1000, + power_consumption = "2kW", }, { type = "goods-company", @@ -375,6 +383,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/hightech_facility.png", price = 1000, + power_consumption = "100kW", }, { type = "goods-company", @@ -397,6 +406,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/iron_mine.png", price = 1000, + power_consumption = "1kW", }, { type = "goods-company", @@ -419,6 +429,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/rare_metal_mine.png", price = 1000, + power_consumption = "1kW", }, { type = "goods-company", @@ -438,6 +449,7 @@ data:extend { size = 200.0, asset_location = "assets/sprites/lumber_yard.png", price = 1000, + power_consumption = "100W", }, { type = "goods-company", @@ -460,6 +472,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/woodmill.png", price = 1000, + power_consumption = "5kW", }, { type = "goods-company", @@ -481,6 +494,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/furniture_store.png", price = 1000, + power_consumption = "500W", }, { type = "goods-company", @@ -503,6 +517,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/foundry.png", price = 1000, + power_consumption = "10kW", }, { type = "goods-company", @@ -525,6 +540,7 @@ data:extend { size = 50.0, asset_location = "assets/sprites/slaughterhouse.png", price = 1000, + power_consumption = "1kW", }, { type = "goods-company", @@ -544,6 +560,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/animal_farm.png", price = 1000, + power_consumption = "100W", }, { type = "goods-company", @@ -566,6 +583,7 @@ data:extend { size = 80.0, asset_location = "assets/sprites/meat_facility.png", price = 1000, + power_consumption = "1kW", }, { type = "goods-company", @@ -590,5 +608,6 @@ data:extend { filler = "salad.glb", price_per_area = 100, }, + power_consumption = "100W", }, } diff --git a/native_app/src/gui/inspect/inspect_building.rs b/native_app/src/gui/inspect/inspect_building.rs index f8eab91a..7c9d4c1a 100644 --- a/native_app/src/gui/inspect/inspect_building.rs +++ b/native_app/src/gui/inspect/inspect_building.rs @@ -7,9 +7,9 @@ use simulation::{Simulation, SoulID}; use crate::gui::inspect::entity_link; use crate::gui::item_icon; use egui_inspect::{Inspect, InspectArgs, InspectVec2Rotation}; -use prototypes::{ItemID, Recipe}; +use prototypes::{ItemID, Power, Recipe}; use simulation::map::{Building, BuildingID, BuildingKind, Zone, MAX_ZONE_AREA}; -use simulation::map_dynamic::BuildingInfos; +use simulation::map_dynamic::{BuildingInfos, ElectricityFlow}; use simulation::souls::freight_station::FreightTrainState; /// Inspect a specific building, showing useful information about it @@ -136,8 +136,12 @@ fn render_goodscompany(ui: &mut Ui, uiworld: &mut UiWorld, sim: &Simulation, b: }; let goods = &c.comp; let workers = &c.workers; + let proto = c.comp.proto.prototype(); + + let market = &*sim.read::(); + let map = &*sim.map(); + let elec_flow = &*sim.read::(); - let market = sim.read::(); let max_workers = goods.max_workers; egui::ProgressBar::new(workers.0.len() as f32 / max_workers as f32) .text(format!("workers: {}/{}", workers.0.len(), max_workers)) @@ -149,16 +153,51 @@ fn render_goodscompany(ui: &mut Ui, uiworld: &mut UiWorld, sim: &Simulation, b: entity_link(uiworld, sim, ui, driver); }); } - let productivity = goods.productivity(workers.0.len(), b.zone.as_ref()); - let productivity = (productivity * 100.0).round(); - if productivity < 100.0 { + let productivity = c.productivity(proto, b.zone.as_ref(), map, elec_flow); + if productivity < 1.0 { egui::ProgressBar::new(productivity) - .text(format!("productivity: {productivity:.0}%")) + .text(format!( + "productivity: {:.0}%", + (productivity * 100.0).round() + )) .desired_width(200.0) .ui(ui); } - render_recipe(ui, uiworld, &goods.proto.prototype().recipe); + if let Some(ref r) = proto.recipe { + render_recipe(ui, uiworld, r); + } + + if let Some(net_id) = map.electricity.net_id(b.id) { + let elec_productivity = elec_flow.productivity(net_id); + + if proto.power_consumption > Power::ZERO { + egui::ProgressBar::new(productivity * elec_productivity) + .text(format!( + "power: {}/{}", + (productivity * elec_productivity) as f64 * proto.power_consumption, + proto.power_consumption + )) + .desired_width(200.0) + .ui(ui); + } + + if proto.power_production > Power::ZERO { + ui.label(format!( + "producing power: {}", + proto.power_production * productivity as f64 + )); + + let stats = elec_flow.network_stats(net_id); + egui::ProgressBar::new(elec_productivity) + .text(format!( + "Network health: {}/{}", + stats.produced_power, stats.consumed_power + )) + .desired_width(200.0) + .ui(ui); + } + } egui::ProgressBar::new(goods.progress) .show_percentage() diff --git a/native_app/src/gui/topgui.rs b/native_app/src/gui/topgui.rs index 4d003cd8..dc52f880 100644 --- a/native_app/src/gui/topgui.rs +++ b/native_app/src/gui/topgui.rs @@ -20,6 +20,7 @@ use egui_inspect::{Inspect, InspectArgs}; use geom::{Polygon, Vec2}; use prototypes::{ prototypes_iter, BuildingGen, FreightStationPrototype, GoodsCompanyPrototype, ItemID, Money, + Power, }; use serde::{Deserialize, Serialize}; use simulation::economy::Government; @@ -620,26 +621,41 @@ impl Gui { .resizable(false) .show(ui.ctx(), |ui| { ui.label(format!("workers: {}", descr.n_workers)); - ui.add_space(10.0); - if !descr.recipe.consumption.is_empty() { - ui.label("consumption:"); - for item in &descr.recipe.consumption { - item_icon(ui, uiworld, item.id, item.amount); + + if let Some(ref recipe) = descr.recipe { + ui.add_space(10.0); + if !recipe.consumption.is_empty() { + ui.label("consumption:"); + for item in &recipe.consumption { + item_icon(ui, uiworld, item.id, item.amount); + } + ui.add_space(10.0); + } + if !recipe.production.is_empty() { + ui.label("production:"); + for item in &recipe.production { + item_icon(ui, uiworld, item.id, item.amount); + } + ui.add_space(10.0); } + ui.label(format!("time: {}s", recipe.complexity)); + ui.label(format!( + "storage multiplier: {}", + recipe.storage_multiplier + )); + } + + if descr.power_consumption > Power::ZERO { ui.add_space(10.0); + ui.label(format!("Power: {}", descr.power_consumption)); } - if !descr.recipe.production.is_empty() { - ui.label("production:"); - for item in &descr.recipe.production { - item_icon(ui, uiworld, item.id, item.amount); - } + if descr.power_production > Power::ZERO { ui.add_space(10.0); + ui.label(format!( + "Power production: {}", + descr.power_production + )); } - ui.label(format!("time: {}s", descr.recipe.complexity)); - ui.label(format!( - "storage multiplier: {}", - descr.recipe.storage_multiplier - )); }); } }); diff --git a/prototypes/src/lib.rs b/prototypes/src/lib.rs index 139f08d2..b8ae7dcc 100644 --- a/prototypes/src/lib.rs +++ b/prototypes/src/lib.rs @@ -122,7 +122,7 @@ pub fn try_prototype(id: ID) -> Option<&'static ::Prototype::storage(try_prototypes()?).get(&id) + ::Prototype::storage(prototypes()).get(&id) } #[inline] diff --git a/prototypes/src/macros.rs b/prototypes/src/macros.rs index 1d840c05..60a2d3e1 100644 --- a/prototypes/src/macros.rs +++ b/prototypes/src/macros.rs @@ -52,6 +52,7 @@ macro_rules! gen_prototypes { &prototypes.orderings.$name } + #[inline] fn storage(prototypes: &Prototypes) -> &common::TransparentMap { &prototypes.$name } diff --git a/prototypes/src/prototypes/company.rs b/prototypes/src/prototypes/company.rs index 7d180585..a4e2e90e 100644 --- a/prototypes/src/prototypes/company.rs +++ b/prototypes/src/prototypes/company.rs @@ -1,5 +1,7 @@ use crate::prototypes::PrototypeBase; -use crate::{get_with_err, GoodsCompanyID, Money, NoParent, Prototype, Recipe, Size2D, Zone}; +use crate::{ + get_with_err, GoodsCompanyID, Money, NoParent, Power, Prototype, Recipe, Size2D, Zone, +}; use egui_inspect::{debug_inspect_impl, Inspect}; use geom::Vec2; use mlua::{FromLua, Lua, Table, Value}; @@ -25,8 +27,6 @@ pub enum CompanyKind { Store, /// Buyers get their goods delivered to them Factory, - /// Buyers get their goods instantly delivered, useful for things like electricity/water/.. - Network, } #[derive(Debug, Clone)] @@ -35,12 +35,14 @@ pub struct GoodsCompanyPrototype { pub id: GoodsCompanyID, pub bgen: BuildingGen, pub kind: CompanyKind, - pub recipe: Recipe, - pub n_trucks: i32, - pub n_workers: i32, + pub recipe: Option, + pub n_trucks: u32, + pub n_workers: u32, pub size: Size2D, pub asset_location: String, pub price: Money, + pub power_consumption: Power, + pub power_production: Power, pub zone: Option, } @@ -57,11 +59,13 @@ impl Prototype for GoodsCompanyPrototype { bgen: get_with_err(table, "bgen")?, kind: get_with_err(table, "kind")?, recipe: get_with_err(table, "recipe")?, - n_trucks: table.get::<_, Option>("n_trucks")?.unwrap_or(0), - n_workers: get_with_err(table, "n_workers")?, + n_trucks: get_with_err::>(table, "n_trucks")?.unwrap_or(0), + n_workers: get_with_err::>(table, "n_workers")?.unwrap_or(0), size: get_with_err(table, "size")?, asset_location: get_with_err(table, "asset_location")?, price: get_with_err(table, "price")?, + power_consumption: get_with_err(table, "power_consumption")?, + power_production: get_with_err(table, "power_production")?, zone: get_with_err(table, "zone").ok(), }) } @@ -85,7 +89,6 @@ impl<'a> FromLua<'a> for CompanyKind { match &*s { "store" => Ok(Self::Store), "factory" => Ok(Self::Factory), - "network" => Ok(Self::Network), _ => Err(mlua::Error::external(format!( "Unknown company kind: {}", s diff --git a/prototypes/src/prototypes/solar.rs b/prototypes/src/prototypes/solar.rs index b3f903db..c352d3aa 100644 --- a/prototypes/src/prototypes/solar.rs +++ b/prototypes/src/prototypes/solar.rs @@ -1,12 +1,10 @@ -use crate::{get_with_err, GoodsCompanyPrototype, Power, Prototype, SolarPanelID}; +use crate::{GoodsCompanyPrototype, Prototype, SolarPanelID}; use std::ops::Deref; #[derive(Debug, Clone)] pub struct SolarPanelPrototype { pub base: GoodsCompanyPrototype, pub id: SolarPanelID, - /// The maximum power output when the sun is at its peak - pub max_power: Power, } impl Prototype for SolarPanelPrototype { @@ -19,7 +17,6 @@ impl Prototype for SolarPanelPrototype { Ok(Self { id: SolarPanelID::new(&base.name), base, - max_power: get_with_err(table, "max_power")?, }) } diff --git a/prototypes/src/types/power.rs b/prototypes/src/types/power.rs index 12558030..0fddee5c 100644 --- a/prototypes/src/types/power.rs +++ b/prototypes/src/types/power.rs @@ -5,7 +5,7 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; use thiserror::Error; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] /// Power in watts (J/s) pub struct Power(pub i64); debug_inspect_impl!(Power); @@ -80,7 +80,13 @@ impl Display for Power { _ => ("GW", 1_000_000_000.0), }; - write!(f, "{:.2}{}", self.0 as f64 / div, unit) + let v = self.0 as f64 / div; + + if (v.round() - v).abs() < 0.01 { + write!(f, "{}{}", v.round(), unit) + } else { + write!(f, "{:.2}{}", v, unit) + } } } @@ -110,3 +116,91 @@ impl<'lua> FromLua<'lua> for Power { } } } + +impl std::ops::Mul for f64 { + type Output = Power; + + fn mul(self, rhs: Power) -> Self::Output { + Power((self * rhs.0 as f64) as i64) + } +} + +impl std::ops::Mul for i64 { + type Output = Power; + + fn mul(self, rhs: Power) -> Self::Output { + Power(self * rhs.0) + } +} + +impl std::ops::Neg for Power { + type Output = Power; + + fn neg(self) -> Self::Output { + Power(-self.0) + } +} + +impl std::iter::Sum for Power { + fn sum>(iter: I) -> Self { + iter.fold(Power::ZERO, |a, b| a + b) + } +} + +impl std::fmt::Debug for Power { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self, f) + } +} + +impl std::ops::Sub for Power { + type Output = Power; + + fn sub(self, other: Power) -> Power { + Power(self.0 - other.0) + } +} + +impl std::ops::SubAssign for Power { + fn sub_assign(&mut self, other: Power) { + self.0 -= other.0; + } +} + +impl std::ops::Add for Power { + type Output = Power; + + fn add(self, other: Power) -> Power { + Power(self.0 + other.0) + } +} + +impl std::ops::AddAssign for Power { + fn add_assign(&mut self, other: Power) { + self.0 += other.0; + } +} + +impl std::ops::Mul for Power { + type Output = Power; + + fn mul(self, rhs: i64) -> Self::Output { + Power(self.0 * rhs) + } +} + +impl std::ops::Mul for Power { + type Output = Power; + + fn mul(self, rhs: f64) -> Self::Output { + Power((self.0 as f64 * rhs) as i64) + } +} + +impl std::ops::Div for Power { + type Output = Power; + + fn div(self, rhs: i64) -> Self::Output { + Power(self.0 / rhs) + } +} diff --git a/prototypes/src/validation.rs b/prototypes/src/validation.rs index 7fd660d8..d853c10d 100644 --- a/prototypes/src/validation.rs +++ b/prototypes/src/validation.rs @@ -6,7 +6,7 @@ use thiserror::Error; pub enum ValidationError { #[error("{0}: only factories can have trucks")] WrongTrucks(String), - #[error("{0}: factories must have trucks")] + #[error("{0}: factories must have trucks if it produces things")] ZeroTrucks(String), #[error("{0}.{1}: referenced prototype not found")] ReferencedProtoNotFound(String, &'static str), @@ -23,34 +23,49 @@ pub(crate) fn validate(proto: &Prototypes) -> Result<(), MultiError BTreeMap { let mut item_graph: BTreeMap> = BTreeMap::new(); for company in GoodsCompanyPrototype::iter() { - for item in &company.recipe.production { + let Some(ref recipe) = company.recipe else { + continue; + }; + for item in &recipe.production { item_graph.entry(item.id).or_default().push(company.id); } } @@ -363,20 +366,21 @@ fn calculate_prices(price_multiplier: f32) -> BTreeMap { for &comp in item_graph.get(&id).unwrap_or(&vec![]) { let company = &comp.prototype(); let mut price_consumption = Money::ZERO; - for recipe_item in &company.recipe.consumption { + let Some(ref recipe) = company.recipe else { + continue; + }; + for recipe_item in &recipe.consumption { calculate_price_inner(item_graph, recipe_item.id, prices, price_multiplier); price_consumption += prices[&recipe_item.id] * recipe_item.amount as i64; } - let qty = company - .recipe + let qty = recipe .production .iter() .find_map(|x| (x.id == id).then_some(x.amount)) .unwrap_or(0) as i64; - let price_workers = company.recipe.complexity as i64 - * company.n_workers as i64 - * WORKER_CONSUMPTION_PER_SECOND; + let price_workers = + recipe.complexity as i64 * company.n_workers as i64 * WORKER_CONSUMPTION_PER_SECOND; let newprice = (price_consumption + Money::new_inner((price_workers.inner() as f32 * price_multiplier) as i64)) diff --git a/simulation/src/init.rs b/simulation/src/init.rs index 3aaf2a6d..3505cf7d 100644 --- a/simulation/src/init.rs +++ b/simulation/src/init.rs @@ -1,8 +1,8 @@ use crate::economy::{market_update, EcoStats, Government, Market}; use crate::map::Map; use crate::map_dynamic::{ - dispatch_system, itinerary_update, routing_changed_system, routing_update_system, - BuildingInfos, Dispatcher, ParkingManagement, + dispatch_system, electricity_flow_system, itinerary_update, routing_changed_system, + routing_update_system, BuildingInfos, Dispatcher, ElectricityFlow, ParkingManagement, }; use crate::multiplayer::MultiplayerState; use crate::souls::freight_station::freight_station_system; @@ -44,6 +44,7 @@ pub fn init() { } } + register_system("electricity_flow_system", electricity_flow_system); register_system("dispatch_system", dispatch_system); register_system("update_decision_system", update_decision_system); register_system("company_system", company_system); @@ -71,6 +72,7 @@ pub fn init() { register_resource_noserialize::>(); register_resource_noinit::("simoptions"); + register_resource_default::("electricity_flow"); register_resource_default::("market"); register_resource_default::("ecostats"); register_resource_default::("multiplayer_state"); diff --git a/simulation/src/map/electricity.rs b/simulation/src/map/electricity_cache.rs similarity index 95% rename from simulation/src/map/electricity.rs rename to simulation/src/map/electricity_cache.rs index 2626e8e0..89f51e64 100644 --- a/simulation/src/map/electricity.rs +++ b/simulation/src/map/electricity_cache.rs @@ -1,9 +1,10 @@ use crate::map::{BuildingID, Buildings, IntersectionID, Intersections, Map, RoadID, Roads}; +use serde::{Deserialize, Serialize}; use std::collections::btree_map::Entry; use std::collections::{BTreeMap, BTreeSet}; /// A network object is an object that can be connected to an electricity network -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum NetworkObjectID { Building(BuildingID), Intersection(IntersectionID), @@ -30,7 +31,7 @@ impl From for NetworkObjectID { /// The id of a network is the id of its lowest object. This is necessary to keep everything /// deterministic even though we don't serialize the electricity cache -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct ElectricityNetworkID(NetworkObjectID); /// A network is a set of sources and sinks that is connected together @@ -185,8 +186,8 @@ impl ElectricityCache { /// Gets the network id of a network object /// Note that network ids change all the time, so this should not be kept as state - pub fn net_id(&self, object_id: NetworkObjectID) -> Option { - self.ids.get(&object_id).copied() + pub fn net_id(&self, object_id: impl Into) -> Option { + self.ids.get(&object_id.into()).copied() } pub fn networks(&self) -> impl Iterator { @@ -369,23 +370,19 @@ impl ElectricityCache { new_network_objects: BTreeSet, ) { let kept_net = cache.networks.get_mut(&kept_network_id).unwrap(); + let mut new_buildings = BTreeSet::new(); for v in new_network_objects.iter() { kept_net.objects.remove(v); if let NetworkObjectID::Building(b) = v { kept_net.buildings.remove(b); + new_buildings.insert(*b); } cache.ids.insert(*v, new_network_id); } let new_network = ElectricityNetwork { id: new_network_id, - buildings: new_network_objects - .iter() - .filter_map(|v| match v { - NetworkObjectID::Building(b) => Some(*b), - _ => None, - }) - .collect(), + buildings: new_buildings, objects: new_network_objects, }; @@ -404,7 +401,7 @@ impl ElectricityCache { #[cfg(test)] mod tests { - use crate::map::electricity::ElectricityCache; + use crate::map::ElectricityCache; use crate::map::{BuildingKind, LanePatternBuilder, Map, MapProject, NetworkObjectID, RoadID}; use common::logger::MyLog; use geom::{vec3, Vec2, OBB}; @@ -483,14 +480,14 @@ mod tests { assert_eq!(e, &mut e_from_map); assert_eq!(e.networks.len(), 1); - assert_eq!(e.networks[&e.net_id(b.into()).unwrap()].objects.len(), 4); - assert_eq!(e.networks[&e.net_id(b.into()).unwrap()].buildings.len(), 1); + assert_eq!(e.networks[&e.net_id(b).unwrap()].objects.len(), 4); + assert_eq!(e.networks[&e.net_id(b).unwrap()].buildings.len(), 1); e.remove_edge(r, b); assert_eq!(e.networks.len(), 2); - assert_eq!(e.networks[&e.net_id(b.into()).unwrap()].buildings.len(), 1); - assert_eq!(e.networks[&e.net_id(r.into()).unwrap()].buildings.len(), 0); + assert_eq!(e.networks[&e.net_id(b).unwrap()].buildings.len(), 1); + assert_eq!(e.networks[&e.net_id(r).unwrap()].buildings.len(), 0); e.add_edge(r, b); @@ -498,6 +495,6 @@ mod tests { let e = &mut m.electricity; assert_eq!(e.networks.len(), 1); - assert_eq!(e.networks[&e.net_id(b.into()).unwrap()].buildings.len(), 1); + assert_eq!(e.networks[&e.net_id(b).unwrap()].buildings.len(), 1); } } diff --git a/simulation/src/map/height_override.rs b/simulation/src/map/height_override.rs index e1b5dc8f..59131ede 100644 --- a/simulation/src/map/height_override.rs +++ b/simulation/src/map/height_override.rs @@ -73,7 +73,7 @@ pub fn find_overrides(map: &mut Map, chunk: SubscriberChunkID) { ProjectKind::Inter(i) => { let i = map.get(i).unwrap(); - let mut bounds = i.bcircle(&map.roads); + let mut bounds = i.bcircle(); bounds.radius *= 2.0; bounds.bbox() @@ -111,7 +111,7 @@ pub fn find_overrides(map: &mut Map, chunk: SubscriberChunkID) { ProjectKind::Inter(i) => { let i = map.get(i).unwrap(); - let mut bounds = i.bcircle(&map.roads); + let mut bounds = i.bcircle(); bounds.radius *= 2.0; setter diff --git a/simulation/src/map/map.rs b/simulation/src/map/map.rs index 775c0de9..d523f30c 100644 --- a/simulation/src/map/map.rs +++ b/simulation/src/map/map.rs @@ -1,4 +1,4 @@ -use crate::map::electricity::ElectricityCache; +use crate::map::electricity_cache::ElectricityCache; use crate::map::height_override::find_overrides; use crate::map::serializing::SerializedMap; use crate::map::{ @@ -215,12 +215,13 @@ impl Map { .dispatch_chunk(UpdateType::Terrain, tree_chunk) }); - self.spatial_map.insert(id, z.poly.clone()); - let toclean = self .spatial_map .query(&z.poly, ProjectFilter::LOT) .collect(); + + self.spatial_map.update(b); + self.clean_lots_inner(toclean); self.check_invariants() @@ -455,8 +456,7 @@ impl Map { inter.update_traffic_control(&mut self.lanes, &self.roads); inter.update_turns(&self.lanes, &self.roads); - self.spatial_map - .update(inter.id, inter.bcircle(&self.roads)); + self.spatial_map.update(inter); } /// Only removes road from Roads and spatial map but keeps lots, buildings connection diff --git a/simulation/src/map/mod.rs b/simulation/src/map/mod.rs index 2919f299..e36a7fe8 100644 --- a/simulation/src/map/mod.rs +++ b/simulation/src/map/mod.rs @@ -28,7 +28,7 @@ pub mod procgen { } mod change_detection; -mod electricity; +mod electricity_cache; mod height_override; mod light_policy; #[allow(clippy::module_inception)] @@ -44,7 +44,7 @@ mod turn_policy; // Use self or else it would be ambiguous with "pathfinding" crate pub use self::pathfinding::*; pub use change_detection::*; -pub use electricity::*; +pub use electricity_cache::*; pub use light_policy::*; pub use map::*; pub use spatial_map::*; diff --git a/simulation/src/map/objects/building.rs b/simulation/src/map/objects/building.rs index 0dc1c329..c8e98d7f 100644 --- a/simulation/src/map/objects/building.rs +++ b/simulation/src/map/objects/building.rs @@ -126,7 +126,7 @@ impl Building { mesh.faces.push((walkway, Color::gray(0.4).into())); } - Some(buildings.insert_with_key(move |id| { + let b = buildings.insert_with_key(move |id| { if let Some(r) = connected_road { if let Some(r) = roads.get_mut(r) { r.connected_buildings.push(id); @@ -135,12 +135,6 @@ impl Building { } } - if let Some(zone) = zone.clone() { - spatial_map.insert(id, zone.poly); - } else { - spatial_map.insert(id, obb); - } - Self { id, mesh, @@ -151,6 +145,9 @@ impl Building { zone, connected_road, } - })) + }); + + spatial_map.insert(&buildings[b]); + Some(b) } } diff --git a/simulation/src/map/objects/intersection.rs b/simulation/src/map/objects/intersection.rs index 2ef91253..f0a40421 100644 --- a/simulation/src/map/objects/intersection.rs +++ b/simulation/src/map/objects/intersection.rs @@ -23,6 +23,7 @@ impl IntersectionID { pub struct Intersection { pub id: IntersectionID, pub pos: Vec3, + pub radius: f32, turns: BTreeSet, @@ -38,12 +39,13 @@ impl Intersection { let id = store.insert_with_key(|id| Intersection { id, pos, + radius: 0.0, turns: Default::default(), roads: Default::default(), turn_policy: Default::default(), light_policy: Default::default(), }); - spatial.insert(id, pos.xy()); + spatial.insert(&store[id]); id } @@ -58,20 +60,24 @@ impl Intersection { }); } - pub fn bcircle(&self, roads: &Roads) -> Circle { + pub fn bcircle(&self) -> Circle { Circle { center: self.pos.xy(), - radius: self - .roads - .iter() - .flat_map(|x| roads.get(*x)) - .map(|x| OrderedFloat(x.interface_from(self.id))) - .max() - .map(|x| x.0) - .unwrap_or(10.0), + radius: self.radius, } } + fn update_radius(&mut self, roads: &Roads) { + self.radius = self + .roads + .iter() + .flat_map(|x| roads.get(*x)) + .map(|x| OrderedFloat(x.interface_from(self.id))) + .max() + .map(|x| x.0) + .unwrap_or(10.0); + } + pub fn remove_road(&mut self, road_id: RoadID) { self.roads.retain(|x| *x != road_id); } @@ -149,6 +155,8 @@ impl Intersection { roads[r1_id].max_interface(id, min_dist); roads[r2_id].max_interface(id, min_dist); } + + self.update_radius(roads); } fn interface_calc(w1: f32, w2: f32, dir1: Vec2, dir2: Vec2) -> f32 { diff --git a/simulation/src/map/objects/lot.rs b/simulation/src/map/objects/lot.rs index 7af38cc9..5d50ea38 100644 --- a/simulation/src/map/objects/lot.rs +++ b/simulation/src/map/objects/lot.rs @@ -52,7 +52,7 @@ impl Lot { shape, height, }); - map.spatial_map.insert(id, shape); + map.spatial_map.insert(&map.lots[id]); Some(id) } @@ -118,8 +118,8 @@ impl Lot { .collect(); let mut rp = |p: Circle| to_remove.extend(map.spatial_map.query(p, ProjectFilter::LOT)); - rp(unwrap_ret!(map.intersections.get(r.src)).bcircle(&map.roads)); - rp(unwrap_ret!(map.intersections.get(r.dst)).bcircle(&map.roads)); + rp(unwrap_ret!(map.intersections.get(r.src)).bcircle()); + rp(unwrap_ret!(map.intersections.get(r.dst)).bcircle()); for lot in to_remove { if let ProjectKind::Lot(lot) = lot { diff --git a/simulation/src/map/objects/road.rs b/simulation/src/map/objects/road.rs index a5e34a97..0d71fea1 100644 --- a/simulation/src/map/objects/road.rs +++ b/simulation/src/map/objects/road.rs @@ -122,7 +122,7 @@ impl Road { road.update_lanes(lanes, parking, env); - spatial.insert(id, road.boldline()); + spatial.insert(road); road.id } diff --git a/simulation/src/map/serializing.rs b/simulation/src/map/serializing.rs index fd143942..6e135259 100644 --- a/simulation/src/map/serializing.rs +++ b/simulation/src/map/serializing.rs @@ -1,8 +1,8 @@ use serde::{Deserialize, Serialize}; use crate::map::{ - BuildingID, Buildings, Environment, Intersections, Lanes, Lots, Map, ParkingSpots, Roads, - SpatialMap, + BuildingID, Buildings, ElectricityCache, Environment, Intersections, Lanes, Lots, Map, + ParkingSpots, Roads, SpatialMap, }; #[derive(Default, Serialize, Deserialize)] @@ -35,7 +35,7 @@ impl From<&Map> for SerializedMap { impl From for Map { fn from(sel: SerializedMap) -> Self { let spatial_map = mk_spatial_map(&sel); - Map { + let mut m = Map { roads: sel.roads, lanes: sel.lanes, intersections: sel.intersections, @@ -46,27 +46,25 @@ impl From for Map { environment: sel.environment, external_train_stations: sel.external_train_stations, ..Self::empty() - } + }; + m.electricity = ElectricityCache::build(&m); + m } } fn mk_spatial_map(m: &SerializedMap) -> SpatialMap { let mut sm = SpatialMap::default(); for b in m.buildings.values() { - if let Some(ref z) = b.zone { - sm.insert(b.id, z.poly.clone()); - continue; - } - sm.insert(b.id, b.obb); + sm.insert(b); } for r in m.roads.values() { - sm.insert(r.id, r.boldline()); + sm.insert(r); } for i in m.intersections.values() { - sm.insert(i.id, i.bcircle(&m.roads)); + sm.insert(i); } for l in m.lots.values() { - sm.insert(l.id, l.shape); + sm.insert(l); } sm } diff --git a/simulation/src/map/spatial_map.rs b/simulation/src/map/spatial_map.rs index a0fd720a..5a2fbdcc 100644 --- a/simulation/src/map/spatial_map.rs +++ b/simulation/src/map/spatial_map.rs @@ -1,4 +1,7 @@ -use crate::map::{BuildingID, CanonicalPosition, IntersectionID, LotID, Map, RoadID}; +use crate::map::{ + Building, BuildingID, CanonicalPosition, Intersection, IntersectionID, Lot, LotID, Map, Road, + RoadID, +}; use derive_more::From; use flat_spatial::aabbgrid::AABBGridHandle; use flat_spatial::AABBGrid; @@ -69,6 +72,11 @@ impl ProjectKind { } } +pub trait SpatialMapObject { + fn kind(&self) -> ProjectKind; + fn shape(&self) -> ShapeEnum; +} + pub struct SpatialMap { broad: AABBGrid, near: BTreeMap, @@ -86,9 +94,9 @@ impl Default for SpatialMap { } impl SpatialMap { - pub fn insert(&mut self, kind: impl Into, shape: impl Into) { - let kind = kind.into(); - let shape = shape.into(); + pub fn insert(&mut self, obj: &impl SpatialMapObject) { + let kind = obj.kind(); + let shape = obj.shape(); let handle = self.broad.insert(shape.bbox(), kind); if let Some(old_handle) = self.ids.insert(kind, handle) { self.broad.remove(old_handle); @@ -109,9 +117,9 @@ impl SpatialMap { } } - pub fn update(&mut self, kind: impl Into, shape: impl Into) { - let kind = kind.into(); - let shape = shape.into(); + pub fn update(&mut self, obj: &impl SpatialMapObject) { + let kind = obj.kind(); + let shape = obj.shape(); if let Some(id) = self.ids.get(&kind) { self.broad.set_aabb(*id, shape.bbox()); self.near.insert(kind, shape); @@ -163,6 +171,49 @@ impl SpatialMap { } } +impl SpatialMapObject for Intersection { + fn kind(&self) -> ProjectKind { + ProjectKind::Inter(self.id) + } + + fn shape(&self) -> ShapeEnum { + self.bcircle().into() + } +} + +impl SpatialMapObject for Road { + fn kind(&self) -> ProjectKind { + ProjectKind::Road(self.id) + } + + fn shape(&self) -> ShapeEnum { + self.boldline().into() + } +} + +impl SpatialMapObject for Lot { + fn kind(&self) -> ProjectKind { + ProjectKind::Lot(self.id) + } + + fn shape(&self) -> ShapeEnum { + self.shape.into() + } +} + +impl SpatialMapObject for Building { + fn kind(&self) -> ProjectKind { + ProjectKind::Building(self.id) + } + + fn shape(&self) -> ShapeEnum { + if let Some(ref z) = self.zone { + return z.poly.clone().into(); + } + self.obb.into() + } +} + #[derive(Copy, Clone)] pub struct ProjectFilter(u8); diff --git a/simulation/src/map_dynamic/electricity.rs b/simulation/src/map_dynamic/electricity.rs new file mode 100644 index 00000000..abf93e56 --- /dev/null +++ b/simulation/src/map_dynamic/electricity.rs @@ -0,0 +1,93 @@ +use crate::map::{BuildingKind, ElectricityNetworkID, Map}; +use crate::map_dynamic::BuildingInfos; +use crate::utils::resources::Resources; +use crate::{SoulID, World}; +use common::FastMap; +use prototypes::Power; +use serde::Deserialize; +use slotmapd::__impl::Serialize; + +#[derive(Default, Serialize, Deserialize)] +pub struct ElectricityFlow { + flowmap: FastMap, +} + +impl ElectricityFlow { + pub fn productivity(&self, network: ElectricityNetworkID) -> f32 { + self.flowmap + .get(&network) + .map(|f| f.productivity) + .unwrap_or(1.0) + } + + pub fn network_stats(&self, network: ElectricityNetworkID) -> NetworkFlow { + self.flowmap.get(&network).cloned().unwrap_or(NetworkFlow { + consumed_power: Power::ZERO, + produced_power: Power::ZERO, + productivity: 1.0, + }) + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct NetworkFlow { + pub consumed_power: Power, + pub produced_power: Power, + /// The productivity of the network, between 0 and 1 + /// Ratio of the power produced by the network compared to the power consumed capped to 1 + pub productivity: f32, +} + +pub fn electricity_flow_system(world: &mut World, resources: &mut Resources) { + let map = resources.read::(); + let binfos = resources.read::(); + let mut flow = resources.write::(); + + flow.flowmap.clear(); + + for network in map.electricity.networks.values() { + let mut consumed_power: Power = Power::ZERO; + let mut produced_power: Power = Power::ZERO; + + for building in network.buildings.iter() { + let building = map.buildings.get(*building).unwrap(); + + match building.kind { + BuildingKind::House => { + consumed_power += Power::new(100); + } + BuildingKind::GoodsCompany(comp) => { + let proto = comp.prototype(); + + let Some(SoulID::GoodsCompany(owner)) = binfos.owner(building.id) else { + continue; + }; + + let Some(ent) = world.companies.get(owner) else { + continue; + }; + let productivity = ent.raw_productivity(proto, building.zone.as_ref()) as f64; + + consumed_power += proto.power_consumption * productivity; + produced_power += proto.power_production * productivity; + } + BuildingKind::RailFreightStation(_) => {} + BuildingKind::TrainStation => {} + BuildingKind::ExternalTrading => {} + } + } + + flow.flowmap.insert( + network.id, + NetworkFlow { + consumed_power, + produced_power, + productivity: if consumed_power == Power::ZERO { + 1.0 + } else { + f64::min(produced_power.0 as f64 / consumed_power.0 as f64, 1.0) as f32 + }, + }, + ); + } +} diff --git a/simulation/src/map_dynamic/mod.rs b/simulation/src/map_dynamic/mod.rs index 67dbe417..0e272e11 100644 --- a/simulation/src/map_dynamic/mod.rs +++ b/simulation/src/map_dynamic/mod.rs @@ -1,11 +1,13 @@ mod binfos; mod dispatch; +mod electricity; mod itinerary; mod parking; mod router; pub use binfos::*; pub use dispatch::*; +pub use electricity::*; pub use itinerary::*; pub use parking::*; pub use router::*; diff --git a/simulation/src/souls/goods_company.rs b/simulation/src/souls/goods_company.rs index 04d9afae..3924de56 100644 --- a/simulation/src/souls/goods_company.rs +++ b/simulation/src/souls/goods_company.rs @@ -2,17 +2,17 @@ use serde::{Deserialize, Serialize}; use egui_inspect::Inspect; use geom::{Transform, Vec2}; -use prototypes::{CompanyKind, GoodsCompanyID, ItemID, Recipe}; +use prototypes::{CompanyKind, GoodsCompanyID, GoodsCompanyPrototype, ItemID, Power, Recipe}; use crate::economy::{find_trade_place, Market}; use crate::map::{Building, BuildingID, Map, Zone, MAX_ZONE_AREA}; -use crate::map_dynamic::BuildingInfos; +use crate::map_dynamic::{BuildingInfos, ElectricityFlow}; use crate::souls::desire::WorkKind; use crate::transportation::{spawn_parked_vehicle, VehicleKind}; use crate::utils::resources::Resources; use crate::utils::time::GameTime; use crate::world::{CompanyEnt, HumanEnt, HumanID, VehicleID}; -use crate::{ParCommandBuffer, SoulID}; +use crate::{ParCommandBuffer, SoulID, VehicleEnt}; use crate::{Simulation, World}; use super::desire::Work; @@ -36,6 +36,8 @@ pub fn recipe_should_produce(recipe: &Recipe, soul: SoulID, market: &Market) -> recipe.production.iter().all(move |item| { market.capital(soul, item.id) < item.amount * (recipe.storage_multiplier + 1) }) + // has something to do + && (!recipe.consumption.is_empty() || !recipe.production.is_empty()) } pub fn recipe_act(recipe: &Recipe, soul: SoulID, near: Vec2, market: &mut Market) { @@ -58,16 +60,44 @@ pub fn recipe_act(recipe: &Recipe, soul: SoulID, near: Vec2, market: &mut Market pub struct GoodsCompanyState { pub proto: GoodsCompanyID, pub building: BuildingID, - pub max_workers: i32, + pub max_workers: u32, /// In [0; 1] range, to show how much has been made until new product pub progress: f32, pub driver: Option, pub trucks: Vec, } -impl GoodsCompanyState { - pub fn productivity(&self, workers: usize, zone: Option<&Zone>) -> f32 { - workers as f32 / self.max_workers as f32 * zone.map_or(1.0, |z| z.area / MAX_ZONE_AREA) +impl CompanyEnt { + /// Returns the productivity of the company, in [0; 1] range _before_ taking electricity into account + pub fn raw_productivity(&self, proto: &GoodsCompanyPrototype, zone: Option<&Zone>) -> f32 { + let mut p = 1.0; + if proto.n_workers > 0 { + p = self.workers.0.len() as f32 / proto.n_workers as f32; + } + if let Some(z) = zone { + p *= z.area / MAX_ZONE_AREA + } + + p + } + + /// Returns the productivity of the company, in [0; 1] range + pub fn productivity( + &self, + proto: &GoodsCompanyPrototype, + zone: Option<&Zone>, + map: &Map, + elec_flow: &ElectricityFlow, + ) -> f32 { + let mut p = self.raw_productivity(proto, zone); + + if proto.power_consumption > Power::ZERO { + if let Some(net_id) = map.electricity.net_id(self.comp.building) { + p *= elec_flow.productivity(net_id); + } + } + + p } } @@ -91,7 +121,10 @@ pub fn company_soul( for _ in 0..proto.n_trucks { trucks.extend(spawn_parked_vehicle(sim, VehicleKind::Truck, door_pos)) } - if trucks.is_empty() { + if trucks.len() as u32 != proto.n_trucks { + for truck in trucks { + sim.write::>().kill(truck); + } return None; } } @@ -121,10 +154,12 @@ pub fn company_soul( { let m = &mut *sim.write::(); - m.produce(soul, job_opening, company.max_workers); + m.produce(soul, job_opening, company.max_workers as i32); m.sell_all(soul, door_pos.xy(), job_opening, 0); - recipe_init(&proto.recipe, soul, door_pos.xy(), m); + if let Some(ref r) = proto.recipe { + recipe_init(r, soul, door_pos.xy(), m); + } } sim.write::() @@ -141,9 +176,9 @@ pub fn company_system(world: &mut World, res: &mut Resources) { let binfos: &BuildingInfos = &res.read(); let market: &Market = &res.read(); let map: &Map = &res.read(); + let elec_flow: &ElectricityFlow = &res.read(); world.companies.iter_mut().for_each(|(me, c)| { - let n_workers = c.workers.0.len(); let soul = SoulID::GoodsCompany(me); let b: &Building = unwrap_or!(map.buildings.get(c.comp.building), { cbuf.kill(me); @@ -152,22 +187,24 @@ pub fn company_system(world: &mut World, res: &mut Resources) { let proto = c.comp.proto.prototype(); - if recipe_should_produce(&proto.recipe, soul, market) { - c.comp.progress += c.comp.productivity(n_workers, b.zone.as_ref()) - / proto.recipe.complexity as f32 - * delta; - } + if let Some(recipe) = &proto.recipe { + if recipe_should_produce(recipe, soul, market) { + let productivity = c.productivity(proto, b.zone.as_ref(), map, elec_flow); - if c.comp.progress >= 1.0 { - c.comp.progress -= 1.0; - let kind = c.comp.proto; - let bpos = b.door_pos; + c.comp.progress += productivity * delta / recipe.complexity as f32; + } - cbuf.exec_on(me, move |market| { - let recipe = &kind.prototype().recipe; - recipe_act(recipe, soul, bpos.xy(), market); - }); - return; + if c.comp.progress >= 1.0 { + c.comp.progress -= 1.0; + let kind = c.comp.proto; + let bpos = b.door_pos; + + cbuf.exec_on(me, move |market| { + let recipe = kind.prototype().recipe.as_ref().unwrap(); + recipe_act(recipe, soul, bpos.xy(), market); + }); + return; + } } for (_, trades) in c.bought.0.iter_mut() {