Week 14 of 2024
Development log of Otterhide
51 items
- Convert Rust sources to lib + main + bin/ layout
- SQLite: export district names
- In production web build compress all .ron files
- Make nix build work again
- SQLite: export buildings' addresses
- Singularize tables' names in SQLite export
- SQLite: export immigrated events, add ids
- SQLite: Add more columns to building
- Makefile: implement a bunch of check / watch goals
- Makefile: signal errors from schema check
- When checking sqlite-export, start with schema
- Makefile: code change invalidates sqlite-export
- Makefile: In watch mode, always run sqlite-export
- SQLite: Add the sex column to person table
- SQLite export immigration data
- SQLite: export residency data
- Marge simulation and exploration ui into a hud
- Merge population ui into the heads up display
- Fix: No fonts available until first call to Context::run()
- Unify layouts of simulation and exploration controls
- Shrink the gap between central and bottom panels
- Move the population panel down
- Control the central panel via configuration
- Rename "loading" module to "simulation"
- Rename GameState to MajorState
- Separate major state and preloading to own module
- Use gerunds instead of verbs to name states
- Fix: first snapshot restored before districts setup
- Rein in the saved simulation file size
- Fix check/sqlite-export goal in the Makefile
- Implement a HUD progress bar for getting ready
- Tune down most of the logging to debug level
- Speed up SQLite export of large simulations
- Disable World Inspector Egui in release builds
- Disable camera while getting ready
- Remove the obsolete serve goal form our Makefile
- Fix: Camera always disabled
- Assign birth dates to people
- SQLite: include birth dates in exported data
- Fix inconsistent variable naming (birth_date)
- Remove pseudo-PostgreSQL export code
- Fix a typo
- Limit exploration frame to 1h
- Introduce death
- Allow to pass extra arguments to make develop
- Allow setting beginning and duration via arguments
- Fix: residents not restored from snapshots
- Fix crashing with population inspector
- Prevent the stroboscopic effect while simulating
- Don't display sun gizmo in production
- Setup Git LFS, add 2 saved simulations
Convert Rust sources to lib + main + bin/ layout
On by
Now we have a shared library and 2 binaries:
-
otterhide
With entry point in
src/main.rs. It is the main program with the GUI that can run in a web browser (thanks to Trunk) and on desktop. It's purpose is to run the simulation and let users explore it. -
otterhide-to-sqlite
A CLI program that takes a .ron file containing the simulation data (created using the main
otterhideprogram) and converts it to a SQLite .db file. Exporting it to a .sql file is up to an end user (teacher or student). It's trivial to do with thesqlite3command line utility. See thesqlite-exportgoal in ourMakefilefor technical details.
Most code is shared and lives in the library, with an entry point in
src/lib.rs. From there values and types can be imported into both
binaries using the otterhide namespace.
Splitting posed some challenges. Mainly that otterhide-to-sqlite uses
Tokio, an async runtime for Rust. Tokio does not work out of the box in
WebAssembly targets (main platform for the otterhide program). The
solution I chose is to make the tokio and sqlx dependencies
optional, and only require them for the otterhide-to-sqlite binary
that is meant to be run in a terminal. This is done with cli and
sqlite feature flags. They are on by default, but disabled in the
index.html file used by Trunk to compile WebAssembly. See changes in
the Cargo.toml, index.html and Makefile.
Other than that, moving code to a shared library required making some types and functions public. It also exposed some mistakes in examples from doc comments and some unnecessary double referencing (thanks again Clippy 🖇).
index c9ad9af..45aede4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,11 +23,20 @@ regex = "1.10.3"
=ron = "0.8.1"
=serde = "1.0.197"
=serde_qs = "0.12.0"
-sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite"] }
+sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite"], optional = true }
=thiserror = "1.0.58"
-tokio = { version = "1.37.0", features = ["rt", "macros"] }
+tokio = { version = "1.37.0", features = ["rt", "macros"], optional = true }
=wasm-bindgen = "0.2.91"
=web-sys = {version = "0.3.69", features = ["HtmlAnchorElement"]}
=
+[features]
+default = [ "cli", "sqlite" ]
+cli = [ "dep:tokio" ]
+sqlite = [ "dep:sqlx" ]
+
+[[bin]]
+name = "otterhide-to-sqlite"
+required-features = [ "cli", "sqlite" ]
+
=[profile.dev.package."*"]
=opt-level = "z"index 8d50add..5822355 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,7 @@ install: build
=
=develop: ## Rebuild and run a development version of the program
=develop:
- cargo run --features bevy/dynamic_linking
+ cargo run --features bevy/dynamic_linking --bin otterhide
=.PHONY: develop
=
=clean: ## Remove all build artifacts
@@ -51,9 +51,11 @@ sqlit-export: data/sqlite-export.sql
=.PHONY: sqlit-export
=
=data/sqlite-export.sql: data/sqlite-export.db
+ mkdir --parents $(@D)
= sqlite3 $< .dump > $@
=
=data/sqlite-export.db: assets/simulation.ron
+ mkdir --parents $(@D)
= cargo run --bin otterhide-to-sqlite $< $@
=
=index e73b292..b3896cd 100644
--- a/index.html
+++ b/index.html
@@ -38,7 +38,12 @@
= })
= </script>
=
- <link data-trunk rel="rust" data-opt="z" />
+ <link
+ data-trunk rel="rust"
+ data-bin="otterhide"
+ data-cargo-no-default-features
+ data-opt="z"
+ />
=
= </head>
= <body style="touch-action: none">index 6b4613b..7c885e7 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -425,7 +425,7 @@ impl Snapshot for BuildingsSnapshot {
= &HousingCapacity,
= Option<&Residents>,
= ), With<Building>>()
- .iter(&world)
+ .iter(world)
= .map(
= |(id, name, variant, transform, capacity, residents)| BuildingDescription {
= id: id.clone(),index 3ec17bb..fb13c5f 100644
--- a/src/coordinates.rs
+++ b/src/coordinates.rs
@@ -25,7 +25,7 @@ pub struct Longitude;
=/// For example, coordinates of the same dimension can be compared
=///
=/// ```
-/// use otterhide::new_coordinates::*;
+/// use otterhide::coordinates::*;
=///
=/// #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
=/// struct X;index 581caa9..3b96863 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -588,7 +588,7 @@ pub struct DistrictsSnapshot(Vec<DistrictDescription>);
=impl Snapshot for DistrictsSnapshot {
= fn capture(world: &mut World) -> Self {
= let mut districts = world.query::<&DistrictDescription>();
- Self(districts.iter(&world).cloned().collect())
+ Self(districts.iter(world).cloned().collect())
= }
=
= fn restore(&self, world: &mut World) {new file mode 100644
index 0000000..38378ee
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,146 @@
+pub mod buildings;
+pub mod camera;
+pub mod configuration;
+pub mod coordinates;
+pub mod date;
+pub mod day_month;
+pub mod districts;
+pub mod exploration;
+pub mod ground;
+pub mod history;
+pub mod loading; // TODO: The loading module holds the central Simulation type. Rename to simulation.
+pub mod names;
+pub mod parcels;
+pub mod pgsql_export;
+pub mod population;
+pub mod population_ui;
+pub mod roads;
+pub mod ron_export;
+pub mod simulation; // TODO: The simulation module holds only UI code. Merge with other UI modules.
+pub mod sun;
+
+use bevy::prelude::*;
+use bevy::utils::HashSet;
+use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
+use bevy_inspector_egui::prelude::*;
+use date::Date;
+use day_month::DayMonth;
+use day_month::HOUR;
+use history::History;
+use history::HistorySystems;
+pub use loading::Simulation;
+use loading::SimulationAssetHandle;
+
+// TODO: Use configuration to set SimulationParameters.
+// Store them in and restore from Simulation asset.
+/// Immutable parameters of a simulation.
+///
+/// Changing them in the middle of a running simulation will have unspecified
+/// and probably horrible effects. If this parameters are changed, the
+/// simulation has to be re-run.
+#[derive(Debug, Resource, Reflect, InspectorOptions)]
+#[reflect(Resource)]
+pub struct SimulationParameters {
+ /// How big is the landmass on which Otterhide is built
+ pub land_radius: f32,
+ /// Extra distance from the edge of landmass to the sun
+ pub sun_gap: f32,
+ /// Elevate the canter of sun's orbit for longer days
+ pub sun_elevation: f32,
+ // IDEA: We can simplify some calculations by using this value only for Display
+ pub first_year: i32,
+ // The length of the simulation
+ pub years: u16,
+ /// Day-months per one second of real time during simulation
+ #[inspector(min = 2, max = 255, display = NumberDisplay::Slider)]
+ pub frames_per_day: u8,
+ /// What time the first frame of the day starts
+ ///
+ /// 0.0 - midnight. 0.5 - noon
+ pub frame_offset: f32,
+}
+
+impl Default for SimulationParameters {
+ fn default() -> Self {
+ Self {
+ land_radius: 2000.,
+ sun_gap: 200.,
+ sun_elevation: 600.,
+ first_year: 1865,
+ #[cfg(debug_assertions)]
+ years: 3,
+ #[cfg(not(debug_assertions))]
+ years: 150,
+ frame_offset: 2.0 * HOUR,
+ frames_per_day: 6,
+ }
+ }
+}
+
+impl SimulationParameters {
+ pub fn beginning(&self) -> DayMonth {
+ *DayMonth::new(self.first_year).advance(self.frame_offset)
+ }
+ pub fn end(&self) -> DayMonth {
+ *DayMonth::new(self.first_year + self.years as i32).advance(self.frame_offset)
+ }
+}
+
+#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
+pub enum GameState {
+ #[default]
+ Loading,
+ Simulate,
+ Explore,
+}
+
+#[derive(Resource, Default, Debug)]
+pub struct PreloadedAssets(HashSet<UntypedHandle>);
+
+// TODO: The preload_assets function has too many responsibilities. Refactor.
+pub fn preload_assets(
+ server: Res<AssetServer>,
+ mut state: ResMut<NextState<GameState>>,
+ assets: Res<PreloadedAssets>,
+ mut history: ResMut<History>,
+ loaded: Option<Res<SimulationAssetHandle>>,
+ simulations: Res<Assets<Simulation>>,
+ history_systems: Res<HistorySystems>,
+ mut commands: Commands,
+) {
+ let count = assets.0.len();
+
+ info!("Waiting for {count} assets to preload",);
+ let mut ids = assets.0.iter().map(|handle| handle.id());
+ if ids.all(|id| server.is_loaded_with_dependencies(id)) {
+ info!("All {count} assets preloaded!");
+
+ match loaded {
+ None => state.set(GameState::Simulate),
+
+ Some(simulation) => {
+ let simulation = simulations.get(simulation.0.clone()).unwrap();
+
+ info!(
+ "Loaded the simulation asset with {} snapshots and {} events",
+ simulation.history.snapshots.len(),
+ simulation.history.events.len()
+ );
+ *history = simulation.history.clone();
+ let snapshot = history.snapshots[0].clone();
+ commands.run_system_with_input(history_systems.rollback, snapshot);
+ state.set(GameState::Explore)
+ }
+ }
+ }
+}
+
+pub fn switch_states(
+ date: Res<Date>,
+ mut state: ResMut<NextState<GameState>>,
+ simulation: Res<SimulationParameters>,
+) {
+ if date.0 > simulation.end() {
+ state.set(GameState::Explore)
+ }
+}index cba3882..ef16d9c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,53 +1,27 @@
-mod buildings;
-mod camera;
-mod configuration;
-mod coordinates;
-mod date;
-mod day_month;
-mod districts;
-mod exploration;
-mod ground;
-mod history;
-mod loading; // TODO: The loading module holds the central Simulation type. Rename to simulation.
-mod names;
-mod parcels;
-mod pgsql_export;
-mod population;
-mod population_ui;
-mod roads;
-mod ron_export;
-mod simulation; // TODO: The simulation module holds only UI code. Merge with other UI modules.
-mod sun;
-
=use bevy::prelude::*;
-use bevy::utils::HashSet;
=use bevy::utils::Uuid;
-use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
-use bevy_inspector_egui::prelude::*;
=use bevy_inspector_egui::quick::WorldInspectorPlugin;
-use buildings::BuildingsPlugin;
-use camera::CameraPlugin;
-use configuration::ConfigurationPlugin;
-use date::Date;
-use date::DatePlugin;
-use date::NewMonth;
-use day_month::DayMonth;
-use day_month::HOUR;
-use districts::DistrictsPlugin;
-use exploration::ExplorationPlugin;
-use ground::GroundPlugin;
-use history::History;
-use history::HistoryPlugin;
-use history::HistorySystems;
-use loading::Simulation;
-use loading::SimulationAssetHandle;
-use loading::SimulationLoadingPlugin;
-use parcels::ParcelsPlugin;
-use population::PopulationPlugin;
-use population_ui::PopulationUiPlugin;
-use roads::RoadsPlugin;
-use simulation::SimulationPlugin;
-use sun::SunPlugin;
+use otterhide::buildings::BuildingsPlugin;
+use otterhide::camera::CameraPlugin;
+use otterhide::configuration::ConfigurationPlugin;
+use otterhide::date::DatePlugin;
+use otterhide::date::NewMonth;
+use otterhide::districts::DistrictsPlugin;
+use otterhide::exploration::ExplorationPlugin;
+use otterhide::ground::GroundPlugin;
+use otterhide::history::HistoryPlugin;
+use otterhide::loading::SimulationLoadingPlugin;
+use otterhide::parcels::ParcelsPlugin;
+use otterhide::population::PopulationPlugin;
+use otterhide::population_ui::PopulationUiPlugin;
+use otterhide::preload_assets;
+use otterhide::roads::RoadsPlugin;
+use otterhide::simulation::SimulationPlugin;
+use otterhide::sun::SunPlugin;
+use otterhide::switch_states;
+use otterhide::GameState;
+use otterhide::PreloadedAssets;
+use otterhide::SimulationParameters;
=
=fn main() {
= App::new()
@@ -84,128 +58,9 @@ fn main() {
= .add_systems(Startup, greet)
= .add_systems(Update, preload_assets.run_if(in_state(GameState::Loading)))
= .add_systems(Update, switch_states.run_if(on_event::<NewMonth>()))
- .add_systems(OnEnter(GameState::Explore), setup_exploration)
= .run()
=}
=
-// TODO: Use configuration to set SimulationParameters.
-// Store them in and restore from Simulation asset.
-/// Immutable parameters of a simulation.
-///
-/// Changing them in the middle of a running simulation will have unspecified
-/// and probably horrible effects. If this parameters are changed, the
-/// simulation has to be re-run.
-#[derive(Debug, Resource, Reflect, InspectorOptions)]
-#[reflect(Resource)]
-pub struct SimulationParameters {
- /// How big is the landmass on which Otterhide is built
- pub land_radius: f32,
- /// Extra distance from the edge of landmass to the sun
- pub sun_gap: f32,
- /// Elevate the canter of sun's orbit for longer days
- pub sun_elevation: f32,
- // IDEA: We can simplify some calculations by using this value only for Display
- pub first_year: i32,
- // The length of the simulation
- pub years: u16,
- /// Day-months per one second of real time during simulation
- #[inspector(min = 2, max = 255, display = NumberDisplay::Slider)]
- pub frames_per_day: u8,
- /// What time the first frame of the day starts
- ///
- /// 0.0 - midnight. 0.5 - noon
- pub frame_offset: f32,
-}
-
-impl Default for SimulationParameters {
- fn default() -> Self {
- Self {
- land_radius: 2000.,
- sun_gap: 200.,
- sun_elevation: 600.,
- first_year: 1865,
- #[cfg(debug_assertions)]
- years: 3,
- #[cfg(not(debug_assertions))]
- years: 150,
- frame_offset: 2.0 * HOUR,
- frames_per_day: 6,
- }
- }
-}
-
-impl SimulationParameters {
- pub fn beginning(&self) -> DayMonth {
- *DayMonth::new(self.first_year).advance(self.frame_offset)
- }
- pub fn end(&self) -> DayMonth {
- *DayMonth::new(self.first_year + self.years as i32).advance(self.frame_offset)
- }
-}
-
=fn greet() {
= info!("Let's build the city on rock and roll!")
=}
-
-#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
-enum GameState {
- #[default]
- Loading,
- Simulate,
- Explore,
-}
-
-#[derive(Resource, Default, Debug)]
-struct PreloadedAssets(HashSet<UntypedHandle>);
-
-// TODO: The preload_assets function has too many responsibilities. Refactor.
-fn preload_assets(
- server: Res<AssetServer>,
- mut state: ResMut<NextState<GameState>>,
- assets: Res<PreloadedAssets>,
- mut history: ResMut<History>,
- loaded: Option<Res<SimulationAssetHandle>>,
- simulations: Res<Assets<Simulation>>,
- history_systems: Res<HistorySystems>,
- mut commands: Commands,
-) {
- let count = assets.0.len();
-
- info!("Waiting for {count} assets to preload",);
- let mut ids = assets.0.iter().map(|handle| handle.id());
- if ids.all(|id| server.is_loaded_with_dependencies(id)) {
- info!("All {count} assets preloaded!");
-
- match loaded {
- None => state.set(GameState::Simulate),
-
- Some(simulation) => {
- let simulation = simulations.get(simulation.0.clone()).unwrap();
-
- info!(
- "Loaded the simulation asset with {} snapshots and {} events",
- simulation.history.snapshots.len(),
- simulation.history.events.len()
- );
- *history = simulation.history.clone();
- let snapshot = history.snapshots[0].clone();
- commands.run_system_with_input(history_systems.rollback, snapshot);
- state.set(GameState::Explore)
- }
- }
- }
-}
-
-fn switch_states(
- date: Res<Date>,
- mut state: ResMut<NextState<GameState>>,
- simulation: Res<SimulationParameters>,
-) {
- if date.0 > simulation.end() {
- state.set(GameState::Explore)
- }
-}
-
-fn setup_exploration() {
- info!("Now exploring!")
-}SQLite: export district names
On by
It's a proof of concept of reading the simulation .ron file and exporting data based on it. For now there is only one table with only one column, but it's a start :D
index 130ff69..b9c91c6 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -1,11 +1,13 @@
=use anyhow::Context;
=use clap::Parser;
+use otterhide::districts::NewDistrictEstablished;
+use otterhide::history::{EventLogEntry, HistoricalEvent, History};
=use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
=use std::path::PathBuf;
=
=#[derive(Parser)]
=#[command(version, about, long_about = None)]
-struct CLI {
+struct Cli {
= /// Input .ron file
= simulation: PathBuf,
= /// Output .db file
@@ -14,7 +16,16 @@ struct CLI {
=
=#[tokio::main(flavor = "current_thread")]
=async fn main() -> anyhow::Result<()> {
- let cli = CLI::parse();
+ let cli = Cli::parse();
+
+ let input = std::fs::File::open(cli.simulation).context("Opening the input (.ron) file")?;
+
+ let simulation: History =
+ ron::de::from_reader(input).context("Parsing the input (.ron) file")?;
+
+ let count = simulation.events.len();
+
+ eprintln!("About to process {count} events");
=
= if cli.database.exists() {
= std::fs::remove_file(&cli.database).context("Removing previous database")?;
@@ -45,11 +56,20 @@ async fn main() -> anyhow::Result<()> {
= .await
= .context("Creating districts table")?;
=
- sqlx::query("Insert into districts (name) values (?)")
- .bind("Blueberry Hills")
- .execute(&mut *connection)
- .await
- .context("Inserting a district")?;
+ for EventLogEntry { date, event } in simulation.events.iter() {
+ match event {
+ HistoricalEvent::ConstructionOrder(_) => {}
+ HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(description)) => {
+ sqlx::query("Insert into districts (name) values (?)")
+ .bind(description.name.as_str())
+ .execute(&mut *connection)
+ .await
+ .context("Inserting a district")?;
+ }
+ HistoricalEvent::Immigrated(_) => {}
+ HistoricalEvent::MovedIn(_) => {}
+ }
+ }
=
= connection.close().await.context("Closing the connection")
=}In production web build compress all .ron files
On by
It's intended for exported simulations, that currently have aweful sizes. A 3 year worth of simulation produces a 12 MB of .ron file. Compressed with Brottli it becomes ~70kB. This should be ok for students, who will access simulations via GitLab pages and should get the compressed files from there (thanks to content negotiation). However it's still a problem for teachers, who need to handle those huge files.
The bulk of the data are snapshots, that are taken every month. So probably the ultimate solution is to only store events in the exported simulations, and recreate snapshots at runtime. But I don't want to work on this yet, as SQL export is a higher priority. Compression was a quick and easy win.
index 5822355..3e5996f 100644
--- a/Makefile
+++ b/Makefile
@@ -107,8 +107,8 @@ public:
= rm --recursive --force $@
= cp --recursive --force $^ $@
= unset GZIP
- find $@ -type f -regex '.*\.\(htm\|html\|txt\|text\|js\|css\|wasm\)$$' -exec gzip -9 -f -k {} \;
- find $@ -type f -regex '.*\.\(htm\|html\|txt\|text\|js\|css\|wasm\)$$' -exec brotli --force --keep {} \;
+ find $@ -type f -regex '.*\.\(htm\|html\|txt\|text\|js\|css\|wasm\|ron\)$$' -exec gzip -9 -f -k {} \;
+ find $@ -type f -regex '.*\.\(htm\|html\|txt\|text\|js\|css\|wasm\|ron\)$$' -exec brotli --force --keep {} \;
= touch $@
=.PHONY: public
=Make nix build work again
On by
It was refusing to build a package because of the unpure git dependency for bevy-inspector-egui. Adding a checksum makes it pure in the eyes of Nix, and thus the project is buildable.
The main otterhide program built this way won't run because the assets
are not there. Should be an easy fix, but I don't care enough right now.
Later.
index 35fed79..203b60f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -60,6 +60,9 @@
= src = ./.;
= cargoLock = {
= lockFile = ./Cargo.lock;
+ outputHashes = {
+ "bevy-inspector-egui-0.23.4" = "sha256-hbtkiaFpONeTnQtd2xZM2rIl9A5D4WsatXXtQFgi3Z8=";
+ };
= };
= postFixup = ''
= # See https://discourse.nixos.org/t/rust-bevy-vulkan-loader-and-ld-library-path-variable/25282/2SQLite: export buildings' addresses
On by
Also use a .sql file for setting up schema. The contents of this file are baked into the binary, so it's not needed at runtime. But it's much nicer to develop than strings embedded in Rust code.
index b9c91c6..4ff9218 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -1,5 +1,6 @@
=use anyhow::Context;
=use clap::Parser;
+use otterhide::buildings::ConstructionOrder;
=use otterhide::districts::NewDistrictEstablished;
=use otterhide::history::{EventLogEntry, HistoricalEvent, History};
=use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
@@ -37,31 +38,32 @@ async fn main() -> anyhow::Result<()> {
= let pool = SqlitePool::connect_with(connect_options)
= .await
= .context("Connecting to the database")?;
+
= let mut connection = pool
= .acquire()
= .await
= .context("Acquiring connection to the database")?;
=
- sqlx::query("Drop table if exists districts")
+ let schema_sql = include_str!("../sqlite-export-schema.sql");
+ sqlx::query(schema_sql)
= .execute(&mut *connection)
= .await
- .context("Dropping districts table")?;
-
- sqlx::query(
- "Create table districts (
- name text
- )",
- )
- .execute(&mut *connection)
- .await
- .context("Creating districts table")?;
+ .context("Setting up the schema")?;
=
= for EventLogEntry { date, event } in simulation.events.iter() {
= match event {
- HistoricalEvent::ConstructionOrder(_) => {}
+ HistoricalEvent::ConstructionOrder(ConstructionOrder(description)) => {
+ let address = description.address.clone();
+ sqlx::query("Insert into buildings (address) values (?)")
+ .bind(address)
+ .execute(&mut *connection)
+ .await
+ .context("Inserting a building")?;
+ }
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(description)) => {
+ let name = description.name.to_string();
= sqlx::query("Insert into districts (name) values (?)")
- .bind(description.name.as_str())
+ .bind(name)
= .execute(&mut *connection)
= .await
= .context("Inserting a district")?;new file mode 100644
index 0000000..a1d2ab6
--- /dev/null
+++ b/src/sqlite-export-schema.sql
@@ -0,0 +1,9 @@
+Drop table if exists districts;
+Create table districts (
+ name text
+);
+
+Drop table if exists buildings;
+Create table buildings (
+ address text
+);Singularize tables' names in SQLite export
On by
I like it singular 🤷
Also print a warning when overriding the database file.
index 4ff9218..c89a9d8 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -28,13 +28,16 @@ async fn main() -> anyhow::Result<()> {
=
= eprintln!("About to process {count} events");
=
- if cli.database.exists() {
- std::fs::remove_file(&cli.database).context("Removing previous database")?;
+ let db_filename = cli.database;
+ if db_filename.exists() {
+ // TODO: Have a --force CLI argument and refuse to overwrite without it
+ eprintln!("Overwriting {}", db_filename.display());
+ std::fs::remove_file(&db_filename).context("Removing previous database")?;
= }
=
= let connect_options = SqliteConnectOptions::new()
= .create_if_missing(true)
- .filename(&cli.database);
+ .filename(&db_filename);
= let pool = SqlitePool::connect_with(connect_options)
= .await
= .context("Connecting to the database")?;
@@ -54,7 +57,7 @@ async fn main() -> anyhow::Result<()> {
= match event {
= HistoricalEvent::ConstructionOrder(ConstructionOrder(description)) => {
= let address = description.address.clone();
- sqlx::query("Insert into buildings (address) values (?)")
+ sqlx::query("Insert into building (address) values (?)")
= .bind(address)
= .execute(&mut *connection)
= .await
@@ -62,7 +65,7 @@ async fn main() -> anyhow::Result<()> {
= }
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(description)) => {
= let name = description.name.to_string();
- sqlx::query("Insert into districts (name) values (?)")
+ sqlx::query("Insert into district (name) values (?)")
= .bind(name)
= .execute(&mut *connection)
= .awaitindex a1d2ab6..107212c 100644
--- a/src/sqlite-export-schema.sql
+++ b/src/sqlite-export-schema.sql
@@ -1,9 +1,9 @@
-Drop table if exists districts;
-Create table districts (
+Drop table if exists district;
+Create table district (
= name text
=);
=
-Drop table if exists buildings;
-Create table buildings (
+Drop table if exists building;
+Create table building (
= address text
=);SQLite: export immigrated events, add ids
On by
The person and building tables will have an id column now in preapration for residency relation.
index c89a9d8..9b94d4e 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -3,6 +3,7 @@ use clap::Parser;
=use otterhide::buildings::ConstructionOrder;
=use otterhide::districts::NewDistrictEstablished;
=use otterhide::history::{EventLogEntry, HistoricalEvent, History};
+use otterhide::population::Immigrated;
=use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
=use std::path::PathBuf;
=
@@ -55,23 +56,29 @@ async fn main() -> anyhow::Result<()> {
=
= for EventLogEntry { date, event } in simulation.events.iter() {
= match event {
- HistoricalEvent::ConstructionOrder(ConstructionOrder(description)) => {
- let address = description.address.clone();
- sqlx::query("Insert into building (address) values (?)")
- .bind(address)
+ HistoricalEvent::ConstructionOrder(ConstructionOrder(building)) => {
+ sqlx::query("Insert into building (id, address) values (?, ?)")
+ .bind(building.id.to_string())
+ .bind(building.address.clone())
= .execute(&mut *connection)
= .await
= .context("Inserting a building")?;
= }
- HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(description)) => {
- let name = description.name.to_string();
+ HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {
= sqlx::query("Insert into district (name) values (?)")
- .bind(name)
+ .bind(district.name.to_string())
= .execute(&mut *connection)
= .await
= .context("Inserting a district")?;
= }
- HistoricalEvent::Immigrated(_) => {}
+ HistoricalEvent::Immigrated(Immigrated(person)) => {
+ sqlx::query("Insert into person (id, name) values (?, ?)")
+ .bind(person.id.to_string())
+ .bind(person.name.to_string())
+ .execute(&mut *connection)
+ .await
+ .context("Inserting a person")?;
+ }
= HistoricalEvent::MovedIn(_) => {}
= }
= }index 107212c..5227898 100644
--- a/src/sqlite-export-schema.sql
+++ b/src/sqlite-export-schema.sql
@@ -5,5 +5,12 @@ Create table district (
=
=Drop table if exists building;
=Create table building (
+ id uuid primary key not null,
= address text
=);
+
+Drop table if exists person;
+Create table person (
+ id uuid primary key not null,
+ name text
+);SQLite: Add more columns to building
On by
index 9b94d4e..fd64cbd 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -57,12 +57,25 @@ async fn main() -> anyhow::Result<()> {
= for EventLogEntry { date, event } in simulation.events.iter() {
= match event {
= HistoricalEvent::ConstructionOrder(ConstructionOrder(building)) => {
- sqlx::query("Insert into building (id, address) values (?, ?)")
- .bind(building.id.to_string())
- .bind(building.address.clone())
- .execute(&mut *connection)
- .await
- .context("Inserting a building")?;
+ sqlx::query(
+ "Insert into building (
+ id ,
+ address ,
+ construction_year ,
+ construction_month ,
+ longitude ,
+ latitude
+ ) values ( ? , ? , ? , ? , ? , ? )",
+ )
+ .bind(building.id.to_string())
+ .bind(building.address.to_string())
+ .bind(date.year())
+ .bind(date.month() as u8)
+ .bind(building.transform.translation.x)
+ .bind(building.transform.translation.z)
+ .execute(&mut *connection)
+ .await
+ .context("Inserting a building")?;
= }
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {
= sqlx::query("Insert into district (name) values (?)")index 5227898..9c4071e 100644
--- a/src/sqlite-export-schema.sql
+++ b/src/sqlite-export-schema.sql
@@ -6,11 +6,20 @@ Create table district (
=Drop table if exists building;
=Create table building (
= id uuid primary key not null,
- address text
+ address text not null,
+ construction_year integer not null,
+ construction_month integer not null check (construction_month between 1 and 12),
+ longitude real not null,
+ latitude real not null
=);
=
=Drop table if exists person;
=Create table person (
= id uuid primary key not null,
= name text
+ -- TODO: sex text check (text in ("Male", "Female"))
+ -- TODO: date_of_birth
+ -- TODO: date_of_death
+ -- TODO: mother
+ -- TODO: father
=);Makefile: implement a bunch of check / watch goals
On by
To speed up the development of the SQLite export program.
$ make help
check Check the code check/watch Keep checking while watching for changes check/sqlite-export Try exporting the simulation.ron file to SQL check/sqlite-export/watch Watch for code changes and re-try SQLite export check/sqlite-export/schema Check if the SQLite schema file is valid check/sqlite-export/schema/watch Keep checking the schema file while it changes
index 3e5996f..9626efe 100644
--- a/Makefile
+++ b/Makefile
@@ -41,20 +41,55 @@ serve: web
= miniserve --interfaces=127.0.0.1 --index=index.html web
=.PHONY: serve
=
-check: ## Check and recheck the code after every change
+check: ## Check the code
=check:
- watchexec --watch src/ --debounce 1s 'cargo test && cargo clippy'
+ cargo test
+ cargo clippy
=.PHONY: check
=
-sqlit-export: ## Export the simulation.ron file to SQL
-sqlit-export: data/sqlite-export.sql
-.PHONY: sqlit-export
+check/watch: ## Keep checking while watching for changes
+check/watch:
+ watchexec \
+ --watch src/ \
+ --debounce 1s \
+ 'make check'
+.PHONY: check/watch
+
+check/sqlite-export: ## Try exporting the simulation.ron file to SQL
+check/sqlite-export: data/sqlite-export.sql
+.PHONY: check/sqlite-export
+
+check/sqlite-export/watch: ## Watch for code changes and re-try SQLite export
+check/sqlite-export/watch:
+ watchexec \
+ --watch src/ \
+ --debounce 1s \
+ 'make check/sqlite-export --assume-new assets/simulation.ron'
+.PHONY: check/sqlite-export/watch
+
+check/sqlite-export/schema: ## Check if the SQLite schema file is valid
+check/sqlite-export/schema: src/sqlite-export-schema.sql
+ sqlite3 \
+ --init $< \
+ data/sqlite-export.db \
+ .dump
+.PHONY: check/sqlite-export/schema
+
+check/sqlite-export/schema/watch: ## Keep checking the schema file while it changes
+check/sqlite-export/schema/watch:
+ watchexec \
+ --watch src/ \
+ --debounce 1s \
+ 'make check/sqlite-export/schema'
+.PHONY: check/sqlite-export/schema/watch
+
=
=data/sqlite-export.sql: data/sqlite-export.db
= mkdir --parents $(@D)
= sqlite3 $< .dump > $@
=
=data/sqlite-export.db: assets/simulation.ron
+data/sqlite-export.db:
= mkdir --parents $(@D)
= cargo run --bin otterhide-to-sqlite $< $@
=Makefile: signal errors from schema check
On by
Turns out sqlite3 will happily exit with code 0 on invalid SQL, unless given the --bail option.
index 9626efe..72c4ddd 100644
--- a/Makefile
+++ b/Makefile
@@ -71,6 +71,7 @@ check/sqlite-export/schema: ## Check if the SQLite schema file is valid
=check/sqlite-export/schema: src/sqlite-export-schema.sql
= sqlite3 \
= --init $< \
+ --bail \
= data/sqlite-export.db \
= .dump
=.PHONY: check/sqlite-export/schemaWhen checking sqlite-export, start with schema
On by
For a faster feedback loop in development. Checking schema is very fast, while checking full export requires re-compilation of Rust code and takes significantly longer. And if schema is invalid, there is no point in checking the rest.
index 72c4ddd..95bb0ec 100644
--- a/Makefile
+++ b/Makefile
@@ -56,6 +56,7 @@ check/watch:
=.PHONY: check/watch
=
=check/sqlite-export: ## Try exporting the simulation.ron file to SQL
+check/sqlite-export: check/sqlite-export/schema
=check/sqlite-export: data/sqlite-export.sql
=.PHONY: check/sqlite-export
=Makefile: code change invalidates sqlite-export
On by
index 95bb0ec..9fa3d3e 100644
--- a/Makefile
+++ b/Makefile
@@ -65,7 +65,7 @@ check/sqlite-export/watch:
= watchexec \
= --watch src/ \
= --debounce 1s \
- 'make check/sqlite-export --assume-new assets/simulation.ron'
+ 'make check/sqlite-export'
=.PHONY: check/sqlite-export/watch
=
=check/sqlite-export/schema: ## Check if the SQLite schema file is valid
@@ -91,6 +91,7 @@ data/sqlite-export.sql: data/sqlite-export.db
= sqlite3 $< .dump > $@
=
=data/sqlite-export.db: assets/simulation.ron
+data/sqlite-export.db: src/**/*
=data/sqlite-export.db:
= mkdir --parents $(@D)
= cargo run --bin otterhide-to-sqlite $< $@Makefile: In watch mode, always run sqlite-export
On by
Otherwise it never runs, as it's target is considered fresh.
index 9fa3d3e..c5aa041 100644
--- a/Makefile
+++ b/Makefile
@@ -65,7 +65,7 @@ check/sqlite-export/watch:
= watchexec \
= --watch src/ \
= --debounce 1s \
- 'make check/sqlite-export'
+ 'make check/sqlite-export --assume-new assets/simulation.ron'
=.PHONY: check/sqlite-export/watch
=
=check/sqlite-export/schema: ## Check if the SQLite schema file is validSQLite: Add the sex column to person table
On by
It contains Unicode sex symbols. I'm not sure it's such a great idea, but it will be easy to change if need be.
index fd64cbd..e7830d4 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -85,9 +85,14 @@ async fn main() -> anyhow::Result<()> {
= .context("Inserting a district")?;
= }
= HistoricalEvent::Immigrated(Immigrated(person)) => {
- sqlx::query("Insert into person (id, name) values (?, ?)")
+ sqlx::query("Insert into person (
+ id,
+ name,
+ sex
+ ) values (?, ?, ?)")
= .bind(person.id.to_string())
= .bind(person.name.to_string())
+ .bind(person.sex.to_string())
= .execute(&mut *connection)
= .await
= .context("Inserting a person")?;index 9c4071e..05327f1 100644
--- a/src/sqlite-export-schema.sql
+++ b/src/sqlite-export-schema.sql
@@ -16,8 +16,8 @@ Create table building (
=Drop table if exists person;
=Create table person (
= id uuid primary key not null,
- name text
- -- TODO: sex text check (text in ("Male", "Female"))
+ name text not null,
+ sex text check (sex in ('♂', '♀')) not null
= -- TODO: date_of_birth
= -- TODO: date_of_death
= -- TODO: motherSQLite export immigration data
On by
Featuring relation to a person and cross-column constraints around date components (month and year).
index e7830d4..d1e82bd 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -85,17 +85,33 @@ async fn main() -> anyhow::Result<()> {
= .context("Inserting a district")?;
= }
= HistoricalEvent::Immigrated(Immigrated(person)) => {
- sqlx::query("Insert into person (
+ sqlx::query(
+ "Insert into person (
= id,
= name,
= sex
- ) values (?, ?, ?)")
- .bind(person.id.to_string())
- .bind(person.name.to_string())
- .bind(person.sex.to_string())
- .execute(&mut *connection)
- .await
- .context("Inserting a person")?;
+ ) values (?, ?, ?)",
+ )
+ .bind(person.id.to_string())
+ .bind(person.name.to_string())
+ .bind(person.sex.to_string())
+ .execute(&mut *connection)
+ .await
+ .context("Inserting a person")?;
+
+ sqlx::query(
+ "Insert into migration (
+ person,
+ arrival_year,
+ arrival_month
+ ) values (?, ?, ?)",
+ )
+ .bind(person.id.to_string())
+ .bind(date.year())
+ .bind(date.month() as u8)
+ .execute(&mut *connection)
+ .await
+ .context("Inserting immigration data")?;
= }
= HistoricalEvent::MovedIn(_) => {}
= }index 05327f1..e20d098 100644
--- a/src/sqlite-export-schema.sql
+++ b/src/sqlite-export-schema.sql
@@ -23,3 +23,16 @@ Create table person (
= -- TODO: mother
= -- TODO: father
=);
+
+Drop table if exists migration;
+Create table migration (
+ person uuid not null references person(id),
+ arrival_year integer,
+ arrival_month integer check (arrival_month between 1 and 12),
+ departure_year integer,
+ departure_month integer check (arrival_month between 1 and 12),
+
+ -- Neither part of the date can be null, unless both are
+ check ((arrival_year is null) == (arrival_month is null)),
+ check ((departure_year is null) == (departure_month is null))
+);SQLite: export residency data
On by
Creating a many to many relation between buildings and persons.
This is all the data we can extract from simulations as they are now. There are some TODO comments in the schema file, indicating future developments.
index d1e82bd..192351f 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -3,7 +3,7 @@ use clap::Parser;
=use otterhide::buildings::ConstructionOrder;
=use otterhide::districts::NewDistrictEstablished;
=use otterhide::history::{EventLogEntry, HistoricalEvent, History};
-use otterhide::population::Immigrated;
+use otterhide::population::{Immigrated, MovedIn};
=use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
=use std::path::PathBuf;
=
@@ -113,7 +113,23 @@ async fn main() -> anyhow::Result<()> {
= .await
= .context("Inserting immigration data")?;
= }
- HistoricalEvent::MovedIn(_) => {}
+ HistoricalEvent::MovedIn(MovedIn { where_to, who }) => {
+ sqlx::query(
+ "Insert into residency (
+ person,
+ building,
+ from_year,
+ from_month
+ ) values (?, ?, ?, ?)",
+ )
+ .bind(who.to_string())
+ .bind(where_to.to_string())
+ .bind(date.year())
+ .bind(date.month() as u8)
+ .execute(&mut *connection)
+ .await
+ .context("Inserting immigration data")?;
+ }
= }
= }
=index e20d098..1b5c221 100644
--- a/src/sqlite-export-schema.sql
+++ b/src/sqlite-export-schema.sql
@@ -36,3 +36,16 @@ Create table migration (
= check ((arrival_year is null) == (arrival_month is null)),
= check ((departure_year is null) == (departure_month is null))
=);
+
+Drop table if exists residency;
+Create table residency (
+ person uuid not null references person(id),
+ building uuid not null references building(id),
+ from_year integer not null,
+ from_month integer not null check (from_month between 1 and 12),
+ until_year integer,
+ until_month integer check (until_month between 1 and 12),
+
+ -- Neither part of the leave date can be null, unless both are
+ check ((until_year is null) == (until_month is null))
+);Marge simulation and exploration ui into a hud
On by
The old simulation and exploration modules are now merged into a single heads_up_display. Next step is to merge the population_ui too.
Two reasons:
- Egui layout is sensitive to order of execution
- The names were misleading
similarity index 61%
rename from src/exploration.rs
rename to src/heads_up_display.rs
index e68c115..1551c1d 100644
--- a/src/exploration.rs
+++ b/src/heads_up_display.rs
@@ -1,42 +1,100 @@
-use crate::date::Date;
+use crate::date::{Date, DateSystems};
=use crate::history::History;
=use crate::history::HistorySystems;
=use crate::ron_export;
-use crate::GameState;
-use crate::SimulationParameters;
+use crate::{GameState, SimulationParameters};
=use bevy::prelude::*;
-use bevy_egui::egui;
=use bevy_egui::egui::epaint::Shadow;
-use bevy_egui::egui::Frame;
-use bevy_egui::egui::Margin;
=use bevy_egui::egui::Slider;
-use bevy_egui::egui::Stroke;
+use bevy_egui::egui::{self, Frame, Margin, ProgressBar, Stroke};
=use bevy_egui::EguiContexts;
=
-pub struct ExplorationPlugin;
+pub struct HudPlugin;
=
-impl Plugin for ExplorationPlugin {
+impl Plugin for HudPlugin {
= fn build(&self, app: &mut App) {
- app.register_type::<DateSliderValue>()
+ let simulation_system_set = (
+ auto_advance.run_if(resource_equals(AutoAdvanceSimulation(true))),
+ paint_simulation_progress_bar,
+ )
+ .chain();
+ let exploration_system_set = (
+ update_slider_value,
+ paint_replay_controls,
+ apply_slider_value.run_if(resource_changed::<DateSliderValue>),
+ )
+ .chain();
+
+ app.register_type::<AutoAdvanceSimulation>()
+ .init_resource::<AutoAdvanceSimulation>()
+ .register_type::<DateSliderValue>()
= .init_resource::<DateSliderValue>()
= .add_systems(
- PostUpdate,
+ PreUpdate,
= (
- update_slider_value,
- paint_ui,
- apply_slider_value.run_if(resource_changed::<DateSliderValue>),
+ simulation_system_set.run_if(in_state(GameState::Simulate)),
+ exploration_system_set.run_if(in_state(GameState::Explore)),
= )
- .chain()
- .run_if(in_state(GameState::Explore)),
+ .chain(),
= );
= }
=}
=
+#[derive(Resource, Debug, Default, Reflect, PartialEq, Eq)]
+#[reflect(Resource)]
+pub struct AutoAdvanceSimulation(bool);
+
=#[derive(Resource, Reflect, Debug, Default)]
=#[reflect(Resource)]
=struct DateSliderValue(f64);
=
-fn paint_ui(
+fn auto_advance(mut commands: Commands, systems: Res<DateSystems>) {
+ commands.run_system(systems.advance_simulation_date);
+}
+
+fn paint_simulation_progress_bar(
+ mut contexts: EguiContexts,
+ date: Res<Date>,
+ mut auto_advance: ResMut<AutoAdvanceSimulation>,
+ mut commands: Commands,
+ simulation: Res<SimulationParameters>,
+ systems: Res<DateSystems>,
+) {
+ egui::TopBottomPanel::bottom("main")
+ .show_separator_line(false)
+ .frame(Frame {
+ inner_margin: Margin::symmetric(40., 20.),
+ shadow: Shadow::NONE,
+ stroke: Stroke::NONE,
+ ..default()
+ })
+ .show(contexts.ctx_mut(), |ui| {
+ let layout = egui::Layout::left_to_right(egui::Align::Center);
+ ui.with_layout(layout, |ui| {
+ let progress = date.fraction_of_simulation(&simulation);
+ ui.add(ProgressBar::new(progress).text(date.0.year().to_string()));
+
+ if auto_advance.0 {
+ let pause_button = ui.button("⏸");
+ if pause_button.clicked() {
+ auto_advance.0 = false;
+ }
+ } else if ui.input(|i| i.modifiers.ctrl) {
+ let step_button = ui.button("⏯"); // FIXME: Missing glyph
+ if step_button.clicked() {
+ commands.run_system(systems.advance_simulation_date);
+ }
+ } else {
+ let play_button = ui.button("▶");
+ if play_button.clicked() {
+ auto_advance.0 = true;
+ };
+ }
+ })
+ });
+}
+
+fn paint_replay_controls(
= mut contexts: EguiContexts,
= history: Res<History>,
= mut slider_value: ResMut<DateSliderValue>,index 38378ee..52d27dd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,8 +5,8 @@ pub mod coordinates;
=pub mod date;
=pub mod day_month;
=pub mod districts;
-pub mod exploration;
=pub mod ground;
+pub mod heads_up_display;
=pub mod history;
=pub mod loading; // TODO: The loading module holds the central Simulation type. Rename to simulation.
=pub mod names;
@@ -16,7 +16,6 @@ pub mod population;
=pub mod population_ui;
=pub mod roads;
=pub mod ron_export;
-pub mod simulation; // TODO: The simulation module holds only UI code. Merge with other UI modules.
=pub mod sun;
=
=use bevy::prelude::*;index ef16d9c..c6c4c8a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,8 +7,8 @@ use otterhide::configuration::ConfigurationPlugin;
=use otterhide::date::DatePlugin;
=use otterhide::date::NewMonth;
=use otterhide::districts::DistrictsPlugin;
-use otterhide::exploration::ExplorationPlugin;
=use otterhide::ground::GroundPlugin;
+use otterhide::heads_up_display::HudPlugin;
=use otterhide::history::HistoryPlugin;
=use otterhide::loading::SimulationLoadingPlugin;
=use otterhide::parcels::ParcelsPlugin;
@@ -16,7 +16,6 @@ use otterhide::population::PopulationPlugin;
=use otterhide::population_ui::PopulationUiPlugin;
=use otterhide::preload_assets;
=use otterhide::roads::RoadsPlugin;
-use otterhide::simulation::SimulationPlugin;
=use otterhide::sun::SunPlugin;
=use otterhide::switch_states;
=use otterhide::GameState;
@@ -51,8 +50,7 @@ fn main() {
= .add_plugins(SunPlugin)
= .add_plugins(HistoryPlugin)
= .add_plugins(WorldInspectorPlugin::new())
- .add_plugins(SimulationPlugin)
- .add_plugins(ExplorationPlugin)
+ .add_plugins(HudPlugin)
= .add_plugins(PopulationPlugin)
= .add_plugins(PopulationUiPlugin)
= .add_systems(Startup, greet)deleted file mode 100644
index e735fb0..0000000
--- a/src/simulation.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-use crate::date::{Date, DateSystems};
-use crate::{GameState, SimulationParameters};
-use bevy::prelude::*;
-use bevy_egui::{
- egui::{self, epaint::Shadow, Frame, Margin, ProgressBar, Stroke},
- EguiContexts,
-};
-
-pub struct SimulationPlugin;
-
-impl Plugin for SimulationPlugin {
- fn build(&self, app: &mut App) {
- app.register_type::<AutoAdvance>()
- .init_resource::<AutoAdvance>()
- .add_systems(
- PreUpdate,
- auto_advance.run_if(
- resource_equals(AutoAdvance(true)).and_then(in_state(GameState::Simulate)),
- ),
- )
- .add_systems(Update, paint_ui.run_if(in_state(GameState::Simulate)));
- }
-}
-
-#[derive(Resource, Debug, Default, Reflect, PartialEq, Eq)]
-#[reflect(Resource)]
-pub struct AutoAdvance(bool);
-
-fn auto_advance(mut commands: Commands, systems: Res<DateSystems>) {
- commands.run_system(systems.advance_simulation_date);
-}
-
-fn paint_ui(
- mut contexts: EguiContexts,
- date: Res<Date>,
- mut auto_advance: ResMut<AutoAdvance>,
- mut commands: Commands,
- simulation: Res<SimulationParameters>,
- systems: Res<DateSystems>,
-) {
- egui::TopBottomPanel::bottom("main")
- .show_separator_line(false)
- .frame(Frame {
- inner_margin: Margin::symmetric(40., 20.),
- shadow: Shadow::NONE,
- stroke: Stroke::NONE,
- ..default()
- })
- .show(contexts.ctx_mut(), |ui| {
- let layout = egui::Layout::left_to_right(egui::Align::Center);
- ui.with_layout(layout, |ui| {
- let progress = date.fraction_of_simulation(&simulation);
- ui.add(ProgressBar::new(progress).text(date.0.year().to_string()));
-
- if auto_advance.0 {
- let pause_button = ui.button("⏸");
- if pause_button.clicked() {
- auto_advance.0 = false;
- }
- } else if ui.input(|i| i.modifiers.ctrl) {
- let step_button = ui.button("⏯"); // FIXME: Missing glyph
- if step_button.clicked() {
- commands.run_system(systems.advance_simulation_date);
- }
- } else {
- let play_button = ui.button("▶");
- if play_button.clicked() {
- auto_advance.0 = true;
- };
- }
- })
- });
-}Merge population ui into the heads up display
On by
Finally we can control painting order to prevent overlapping of panels.
index 1551c1d..4420054 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -1,12 +1,25 @@
+use crate::buildings::Building;
+use crate::buildings::BuildingsRegister;
=use crate::date::{Date, DateSystems};
=use crate::history::History;
=use crate::history::HistorySystems;
+use crate::population::Person;
+use crate::population::PersonId;
+use crate::population::PersonsRegister;
+use crate::population::Residence;
+use crate::population::Savings;
+use crate::population::Sex;
=use crate::ron_export;
=use crate::{GameState, SimulationParameters};
=use bevy::prelude::*;
+use bevy_egui::egui;
=use bevy_egui::egui::epaint::Shadow;
+use bevy_egui::egui::Color32;
+use bevy_egui::egui::Frame;
+use bevy_egui::egui::Margin;
+use bevy_egui::egui::ProgressBar;
=use bevy_egui::egui::Slider;
-use bevy_egui::egui::{self, Frame, Margin, ProgressBar, Stroke};
+use bevy_egui::egui::Stroke;
=use bevy_egui::EguiContexts;
=
=pub struct HudPlugin;
@@ -24,6 +37,11 @@ impl Plugin for HudPlugin {
= apply_slider_value.run_if(resource_changed::<DateSliderValue>),
= )
= .chain();
+ let central_panel_system_set = (
+ paint_population_panel,
+ // TODO: Implement other panels
+ // TODO: A mechanism to hide central panel
+ );
=
= app.register_type::<AutoAdvanceSimulation>()
= .init_resource::<AutoAdvanceSimulation>()
@@ -34,6 +52,7 @@ impl Plugin for HudPlugin {
= (
= simulation_system_set.run_if(in_state(GameState::Simulate)),
= exploration_system_set.run_if(in_state(GameState::Explore)),
+ central_panel_system_set,
= )
= .chain(),
= );
@@ -201,3 +220,51 @@ fn apply_slider_value(
=
= commands.run_system_with_input(systems.rollback, snapshot.clone());
=}
+
+fn paint_population_panel(
+ mut contexts: EguiContexts,
+ persons: Query<(&PersonId, &Name, &Sex, &Savings, Option<&Residence>), With<Person>>,
+ buildings: Query<&Name, With<Building>>,
+ persons_register: Res<PersonsRegister>,
+ buildings_register: Res<BuildingsRegister>,
+) {
+ egui::CentralPanel::default()
+ .frame(Frame {
+ inner_margin: Margin::symmetric(40., 20.),
+ outer_margin: Margin::same(40.),
+ shadow: Shadow::NONE,
+ stroke: Stroke::NONE,
+ fill: Color32::from_rgba_premultiplied(20, 20, 20, 240),
+ ..default()
+ })
+ .show(contexts.ctx_mut(), |ui| {
+ let count = persons.into_iter().len();
+ let registered = persons_register.0.len();
+ ui.heading(format!("The {count} ({registered}) People of Otterhide"));
+ egui::ScrollArea::both().show(ui, |ui| {
+ // TODO: Use Table from egui_extras
+ egui::Grid::new("persons-grid").show(ui, |ui| {
+ ui.label("Name");
+ ui.label("Sex");
+ ui.label("Savings");
+ ui.label("Address");
+ ui.label("Id");
+ ui.end_row();
+
+ for (id, name, sex, Savings(savings), residence) in persons.iter() {
+ let address = residence
+ .and_then(|Residence(id)| buildings_register.0.get(id))
+ .map(|entity| buildings.get(*entity).unwrap().as_str())
+ .unwrap_or("-");
+
+ ui.label(name.to_string());
+ ui.label(sex.to_string());
+ ui.label(format!("{savings:.2} Œ"));
+ ui.label(address);
+ ui.label(id.0.to_string());
+ ui.end_row();
+ }
+ })
+ })
+ });
+}index 52d27dd..7250ec1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -13,7 +13,6 @@ pub mod names;
=pub mod parcels;
=pub mod pgsql_export;
=pub mod population;
-pub mod population_ui;
=pub mod roads;
=pub mod ron_export;
=pub mod sun;index c6c4c8a..6657896 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -13,7 +13,6 @@ use otterhide::history::HistoryPlugin;
=use otterhide::loading::SimulationLoadingPlugin;
=use otterhide::parcels::ParcelsPlugin;
=use otterhide::population::PopulationPlugin;
-use otterhide::population_ui::PopulationUiPlugin;
=use otterhide::preload_assets;
=use otterhide::roads::RoadsPlugin;
=use otterhide::sun::SunPlugin;
@@ -52,7 +51,6 @@ fn main() {
= .add_plugins(WorldInspectorPlugin::new())
= .add_plugins(HudPlugin)
= .add_plugins(PopulationPlugin)
- .add_plugins(PopulationUiPlugin)
= .add_systems(Startup, greet)
= .add_systems(Update, preload_assets.run_if(in_state(GameState::Loading)))
= .add_systems(Update, switch_states.run_if(on_event::<NewMonth>()))deleted file mode 100644
index b466c7c..0000000
--- a/src/population_ui.rs
+++ /dev/null
@@ -1,75 +0,0 @@
-use crate::buildings::Building;
-use crate::buildings::BuildingsRegister;
-use crate::population::Person;
-use crate::population::PersonId;
-use crate::population::PersonsRegister;
-use crate::population::Residence;
-use crate::population::Savings;
-use crate::population::Sex;
-use bevy::prelude::*;
-use bevy_egui::egui;
-use bevy_egui::egui::epaint::Shadow;
-use bevy_egui::egui::Color32;
-use bevy_egui::egui::Frame;
-use bevy_egui::egui::Margin;
-use bevy_egui::egui::Stroke;
-use bevy_egui::EguiContexts;
-
-pub struct PopulationUiPlugin;
-
-impl Plugin for PopulationUiPlugin {
- fn build(&self, app: &mut App) {
- app.add_systems(PostUpdate, paint_ui);
- }
-}
-
-// TODO: Make sure this is called after other panels (from simulation and exploration) are painted
-// Otherwise the layout is unstable and sometimes panels overlap.
-
-fn paint_ui(
- mut contexts: EguiContexts,
- persons: Query<(&PersonId, &Name, &Sex, &Savings, Option<&Residence>), With<Person>>,
- buildings: Query<&Name, With<Building>>,
- persons_register: Res<PersonsRegister>,
- buildings_register: Res<BuildingsRegister>,
-) {
- egui::CentralPanel::default()
- .frame(Frame {
- inner_margin: Margin::symmetric(40., 20.),
- outer_margin: Margin::same(40.),
- shadow: Shadow::NONE,
- stroke: Stroke::NONE,
- fill: Color32::from_rgba_premultiplied(20, 20, 20, 240),
- ..default()
- })
- .show(contexts.ctx_mut(), |ui| {
- let count = persons.into_iter().len();
- let registered = persons_register.0.len();
- ui.heading(format!("The {count} ({registered}) People of Otterhide"));
- egui::ScrollArea::both().show(ui, |ui| {
- // TODO: Use Table from egui_extras
- egui::Grid::new("persons-grid").show(ui, |ui| {
- ui.label("Name");
- ui.label("Sex");
- ui.label("Savings");
- ui.label("Address");
- ui.label("Id");
- ui.end_row();
-
- for (id, name, sex, Savings(savings), residence) in persons.iter() {
- let address = residence
- .and_then(|Residence(id)| buildings_register.0.get(id))
- .map(|entity| buildings.get(*entity).unwrap().as_str())
- .unwrap_or("-");
-
- ui.label(name.to_string());
- ui.label(sex.to_string());
- ui.label(format!("{savings:.2} Œ"));
- ui.label(address);
- ui.label(id.0.to_string());
- ui.end_row();
- }
- })
- })
- });
-}Fix: No fonts available until first call to Context::run()
On by
Sometimes the program would panic with this error message. The solution is to make sure that Egui widgets are painted after it's initialized, which happens in PreUpdate schedule. Following a comment at https://github.com/jakobhellermann/bevy-inspector-egui/issues/126#issuecomment-1463452579 I moved all HUD systems to PostUpdate.
index 4420054..f11c06d 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -48,7 +48,7 @@ impl Plugin for HudPlugin {
= .register_type::<DateSliderValue>()
= .init_resource::<DateSliderValue>()
= .add_systems(
- PreUpdate,
+ PostUpdate,
= (
= simulation_system_set.run_if(in_state(GameState::Simulate)),
= exploration_system_set.run_if(in_state(GameState::Explore)),Unify layouts of simulation and exploration controls
On by
index f11c06d..76bfc13 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -79,7 +79,7 @@ fn paint_simulation_progress_bar(
= simulation: Res<SimulationParameters>,
= systems: Res<DateSystems>,
=) {
- egui::TopBottomPanel::bottom("main")
+ egui::TopBottomPanel::bottom("simulation_controls")
= .show_separator_line(false)
= .frame(Frame {
= inner_margin: Margin::symmetric(40., 20.),
@@ -88,28 +88,46 @@ fn paint_simulation_progress_bar(
= ..default()
= })
= .show(contexts.ctx_mut(), |ui| {
- let layout = egui::Layout::left_to_right(egui::Align::Center);
- ui.with_layout(layout, |ui| {
- let progress = date.fraction_of_simulation(&simulation);
- ui.add(ProgressBar::new(progress).text(date.0.year().to_string()));
+ let size = ui.available_size();
+ egui::Grid::new("simulation_controls_grid")
+ .min_col_width(size.x)
+ .show(ui, |ui| {
+ ui.horizontal(|ui| {
+ let min_button_size = egui::Vec2::new(32., 26.);
+ if auto_advance.0 {
+ let pause_button =
+ ui.add(egui::Button::new("⏸").min_size(min_button_size));
+ if pause_button.clicked() {
+ auto_advance.0 = false;
+ }
+ } else if ui.input(|i| i.modifiers.ctrl) {
+ // FIXME: Missing glyph
+ let step_button =
+ ui.add(egui::Button::new("⏯").min_size(min_button_size));
+ if step_button.clicked() {
+ commands.run_system(systems.advance_simulation_date);
+ }
+ } else {
+ let play_button =
+ ui.add(egui::Button::new("▶").min_size(min_button_size));
+ if play_button.clicked() {
+ auto_advance.0 = true;
+ };
+ }
+ });
=
- if auto_advance.0 {
- let pause_button = ui.button("⏸");
- if pause_button.clicked() {
- auto_advance.0 = false;
- }
- } else if ui.input(|i| i.modifiers.ctrl) {
- let step_button = ui.button("⏯"); // FIXME: Missing glyph
- if step_button.clicked() {
- commands.run_system(systems.advance_simulation_date);
- }
- } else {
- let play_button = ui.button("▶");
- if play_button.clicked() {
- auto_advance.0 = true;
- };
- }
- })
+ ui.end_row();
+
+ ui.horizontal(|ui| {
+ let progress = date.fraction_of_simulation(&simulation);
+ ui.add(
+ ProgressBar::new(progress)
+ .text(date.0.year().to_string())
+ .desired_width(size.x)
+ .desired_height(12.0),
+ );
+ });
+ });
= });
=}
=
@@ -119,7 +137,7 @@ fn paint_replay_controls(
= mut slider_value: ResMut<DateSliderValue>,
= mut time: ResMut<Time<Virtual>>,
=) {
- egui::TopBottomPanel::bottom("lower_panel")
+ egui::TopBottomPanel::bottom("explore_controls")
= .show_separator_line(false)
= .frame(Frame {
= inner_margin: Margin::symmetric(40., 20.),Shrink the gap between central and bottom panels
On by
index 76bfc13..6eccdc6 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -82,7 +82,12 @@ fn paint_simulation_progress_bar(
= egui::TopBottomPanel::bottom("simulation_controls")
= .show_separator_line(false)
= .frame(Frame {
- inner_margin: Margin::symmetric(40., 20.),
+ inner_margin: Margin {
+ bottom: 20.,
+ top: 0.,
+ left: 40.,
+ right: 40.,
+ },
= shadow: Shadow::NONE,
= stroke: Stroke::NONE,
= ..default()
@@ -140,7 +145,12 @@ fn paint_replay_controls(
= egui::TopBottomPanel::bottom("explore_controls")
= .show_separator_line(false)
= .frame(Frame {
- inner_margin: Margin::symmetric(40., 20.),
+ inner_margin: Margin {
+ bottom: 20.,
+ top: 0.,
+ left: 40.,
+ right: 40.,
+ },
= shadow: Shadow::NONE,
= stroke: Stroke::NONE,
= ..default()Move the population panel down
On by
So it doesn't overlap with date display.
index 6eccdc6..48ce99e 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -259,7 +259,12 @@ fn paint_population_panel(
= egui::CentralPanel::default()
= .frame(Frame {
= inner_margin: Margin::symmetric(40., 20.),
- outer_margin: Margin::same(40.),
+ outer_margin: Margin {
+ top: 60.,
+ left: 40.,
+ right: 40.,
+ bottom: 10.,
+ },
= shadow: Shadow::NONE,
= stroke: Stroke::NONE,
= fill: Color32::from_rgba_premultiplied(20, 20, 20, 240),Control the central panel via configuration
On by
Now the program takes a new command line option:
--inspect population
In web browser it's
?inspect=Population
Note capital first letter - a discapency between Clap and serde-qs.
The panel will only be shown when this option is present. In the future we will have more panels to chose from. For now that's the only one.
index 3eef085..f720333 100644
--- a/src/configuration.rs
+++ b/src/configuration.rs
@@ -1,3 +1,4 @@
+use crate::heads_up_display::InspectorPanel;
=use bevy::prelude::*;
=use bevy_args::{BevyArgsPlugin, Deserialize, Parser, Serialize};
=
@@ -12,11 +13,16 @@ impl Plugin for ConfigurationPlugin {
=}
=
=#[derive(Resource, Clone, Debug, Default, Serialize, Deserialize, Parser, Reflect)]
+#[reflect(Resource)]
=#[command(about = "Otterhide town simulator", version)]
=pub struct Configuration {
= #[arg(long)]
= #[serde(default)]
= pub load: Option<String>,
+
+ #[arg(long)]
+ #[serde(default)]
+ pub inspect: Option<InspectorPanel>,
=}
=
=fn print_configuration(configuration: Res<Configuration>) {index 48ce99e..24e0b19 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -1,5 +1,6 @@
=use crate::buildings::Building;
=use crate::buildings::BuildingsRegister;
+use crate::configuration::Configuration;
=use crate::date::{Date, DateSystems};
=use crate::history::History;
=use crate::history::HistorySystems;
@@ -10,7 +11,8 @@ use crate::population::Residence;
=use crate::population::Savings;
=use crate::population::Sex;
=use crate::ron_export;
-use crate::{GameState, SimulationParameters};
+use crate::GameState;
+use crate::SimulationParameters;
=use bevy::prelude::*;
=use bevy_egui::egui;
=use bevy_egui::egui::epaint::Shadow;
@@ -21,6 +23,9 @@ use bevy_egui::egui::ProgressBar;
=use bevy_egui::egui::Slider;
=use bevy_egui::egui::Stroke;
=use bevy_egui::EguiContexts;
+use clap::ValueEnum;
+use derive_more::Display;
+use serde::{Deserialize, Serialize};
=
=pub struct HudPlugin;
=
@@ -38,7 +43,7 @@ impl Plugin for HudPlugin {
= )
= .chain();
= let central_panel_system_set = (
- paint_population_panel,
+ paint_population_panel.run_if(inspector_panel_shown(&InspectorPanel::Population)),
= // TODO: Implement other panels
= // TODO: A mechanism to hide central panel
= );
@@ -47,6 +52,7 @@ impl Plugin for HudPlugin {
= .init_resource::<AutoAdvanceSimulation>()
= .register_type::<DateSliderValue>()
= .init_resource::<DateSliderValue>()
+ .register_type::<InspectorPanel>()
= .add_systems(
= PostUpdate,
= (
@@ -59,6 +65,22 @@ impl Plugin for HudPlugin {
= }
=}
=
+#[derive(
+ Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Display, ValueEnum, Reflect,
+)]
+pub enum InspectorPanel {
+ Population,
+}
+
+fn inspector_panel_shown(panel: &InspectorPanel) -> impl FnMut(Res<Configuration>) -> bool + '_ {
+ move |configuration: Res<Configuration>| {
+ configuration
+ .inspect
+ .map(|shown| panel == &shown)
+ .unwrap_or_default()
+ }
+}
+
=#[derive(Resource, Debug, Default, Reflect, PartialEq, Eq)]
=#[reflect(Resource)]
=pub struct AutoAdvanceSimulation(bool);Rename "loading" module to "simulation"
On by
It defines the central Simulation type, so the name is appropriate.
index 7250ec1..6829a91 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -8,13 +8,13 @@ pub mod districts;
=pub mod ground;
=pub mod heads_up_display;
=pub mod history;
-pub mod loading; // TODO: The loading module holds the central Simulation type. Rename to simulation.
=pub mod names;
=pub mod parcels;
=pub mod pgsql_export;
=pub mod population;
=pub mod roads;
=pub mod ron_export;
+pub mod simulation;
=pub mod sun;
=
=use bevy::prelude::*;
@@ -26,8 +26,8 @@ use day_month::DayMonth;
=use day_month::HOUR;
=use history::History;
=use history::HistorySystems;
-pub use loading::Simulation;
-use loading::SimulationAssetHandle;
+pub use simulation::Simulation;
+use simulation::SimulationAssetHandle;
=
=// TODO: Use configuration to set SimulationParameters.
=// Store them in and restore from Simulation asset.index 6657896..2bef918 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,11 +10,11 @@ use otterhide::districts::DistrictsPlugin;
=use otterhide::ground::GroundPlugin;
=use otterhide::heads_up_display::HudPlugin;
=use otterhide::history::HistoryPlugin;
-use otterhide::loading::SimulationLoadingPlugin;
=use otterhide::parcels::ParcelsPlugin;
=use otterhide::population::PopulationPlugin;
=use otterhide::preload_assets;
=use otterhide::roads::RoadsPlugin;
+use otterhide::simulation::SimulationLoadingPlugin;
=use otterhide::sun::SunPlugin;
=use otterhide::switch_states;
=use otterhide::GameState;similarity index 100%
rename from src/loading.rs
rename to src/simulation.rsRename GameState to MajorState
On by
It is not a game.
index 7c885e7..bee15b8 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -3,7 +3,7 @@ use crate::districts::District;
=use crate::history::Snapshot;
=use crate::parcels::{Parcel, ParcelNumber};
=use crate::population::{MovedIn, Person, PersonId};
-use crate::{GameState, PreloadedAssets};
+use crate::{MajorState, PreloadedAssets};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -34,11 +34,11 @@ impl Plugin for BuildingsPlugin {
= .register_type::<BuildingId>()
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_buildings_systems)
- .add_systems(OnExit(GameState::Loading), setup_building_scenes)
+ .add_systems(OnExit(MajorState::Loading), setup_building_scenes)
= .add_systems(
= Update,
= order_construction.run_if(
- in_state(GameState::Simulate)
+ in_state(MajorState::Simulate)
= .and_then(resource_changed::<SimulationFrameCounter>),
= ),
= )index f23bd36..3aa78d9 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -1,6 +1,6 @@
=use crate::day_month::{DayMonth, HOUR, MINUTE};
=use crate::history::Snapshot;
-use crate::{GameState, SimulationParameters};
+use crate::{MajorState, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
@@ -24,7 +24,7 @@ impl Plugin for DatePlugin {
= .add_systems(Startup, register_date_systems)
= .add_systems(
= PreUpdate,
- advance_exploration_date.run_if(in_state(GameState::Explore)),
+ advance_exploration_date.run_if(in_state(MajorState::Explore)),
= )
= .add_systems(PreUpdate, update_date_display);
= }index 3b96863..16b4cfe 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -6,7 +6,7 @@ use crate::names::{DISTRICT_NAMES, DISTRICT_PREFIXES};
=use crate::parcels::{setup_parcels, Parcel};
=use crate::population::Person;
=use crate::roads::{RoadPlan, RoadsSystems};
-use crate::{GameState, PreloadedAssets, SimulationParameters};
+use crate::{MajorState, PreloadedAssets, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -31,11 +31,11 @@ impl Plugin for DistrictsPlugin {
= .add_systems(Startup, register_district_systems)
= .add_systems(Startup, setup_assets)
= // FIXME: The setup_districts system runs after the first snapshot is restored!
- .add_systems(OnExit(GameState::Loading), setup_districts)
+ .add_systems(OnExit(MajorState::Loading), setup_districts)
= .add_systems(
= Update,
= plan_new_districts.after(implement_new_districts).run_if(
- in_state(GameState::Simulate)
+ in_state(MajorState::Simulate)
= .and_then(resource_changed::<SimulationFrameCounter>),
= ),
= )index 24e0b19..22d190b 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -11,7 +11,7 @@ use crate::population::Residence;
=use crate::population::Savings;
=use crate::population::Sex;
=use crate::ron_export;
-use crate::GameState;
+use crate::MajorState;
=use crate::SimulationParameters;
=use bevy::prelude::*;
=use bevy_egui::egui;
@@ -56,8 +56,8 @@ impl Plugin for HudPlugin {
= .add_systems(
= PostUpdate,
= (
- simulation_system_set.run_if(in_state(GameState::Simulate)),
- exploration_system_set.run_if(in_state(GameState::Explore)),
+ simulation_system_set.run_if(in_state(MajorState::Simulate)),
+ exploration_system_set.run_if(in_state(MajorState::Explore)),
= central_panel_system_set,
= )
= .chain(),index 0e943b2..045f82d 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -5,7 +5,7 @@ use crate::districts::DistrictsSnapshot;
=use crate::districts::NewDistrictEstablished;
=use crate::population::{Immigrated, MovedIn, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
-use crate::GameState;
+use crate::MajorState;
=use bevy::ecs::system::{RunSystemOnce, SystemId};
=use bevy::prelude::*;
=use serde::{Deserialize, Serialize};
@@ -19,19 +19,19 @@ impl Plugin for HistoryPlugin {
= app.init_resource::<History>()
= .init_resource::<Future>()
= .add_systems(Startup, setup_history_systems)
- .add_systems(OnEnter(GameState::Simulate), take_snapshot)
+ .add_systems(OnEnter(MajorState::Simulate), take_snapshot)
= .add_systems(
= PreUpdate,
= take_snapshot
- .run_if(in_state(GameState::Simulate).and_then(on_event::<NewMonth>())),
+ .run_if(in_state(MajorState::Simulate).and_then(on_event::<NewMonth>())),
= )
= .add_systems(
= Update,
- register_historical_events.run_if(in_state(GameState::Simulate)),
+ register_historical_events.run_if(in_state(MajorState::Simulate)),
= )
= .add_systems(
= Update,
- replay_historical_events.run_if(in_state(GameState::Explore)),
+ replay_historical_events.run_if(in_state(MajorState::Explore)),
= );
= }
=}
@@ -187,8 +187,8 @@ fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= // after the rollback
= world.run_system_once(clear_pending_historical_events);
=
- world.resource_scope(|_, mut game_state: Mut<NextState<GameState>>| {
- game_state.set(GameState::Explore);
+ world.resource_scope(|_, mut game_state: Mut<NextState<MajorState>>| {
+ game_state.set(MajorState::Explore);
= });
=
= // TODO: DRY on rollback. Maybe a macro?index 6829a91..454dcb6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -85,7 +85,7 @@ impl SimulationParameters {
=}
=
=#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
-pub enum GameState {
+pub enum MajorState {
= #[default]
= Loading,
= Simulate,
@@ -98,7 +98,7 @@ pub struct PreloadedAssets(HashSet<UntypedHandle>);
=// TODO: The preload_assets function has too many responsibilities. Refactor.
=pub fn preload_assets(
= server: Res<AssetServer>,
- mut state: ResMut<NextState<GameState>>,
+ mut state: ResMut<NextState<MajorState>>,
= assets: Res<PreloadedAssets>,
= mut history: ResMut<History>,
= loaded: Option<Res<SimulationAssetHandle>>,
@@ -114,7 +114,7 @@ pub fn preload_assets(
= info!("All {count} assets preloaded!");
=
= match loaded {
- None => state.set(GameState::Simulate),
+ None => state.set(MajorState::Simulate),
=
= Some(simulation) => {
= let simulation = simulations.get(simulation.0.clone()).unwrap();
@@ -127,7 +127,7 @@ pub fn preload_assets(
= *history = simulation.history.clone();
= let snapshot = history.snapshots[0].clone();
= commands.run_system_with_input(history_systems.rollback, snapshot);
- state.set(GameState::Explore)
+ state.set(MajorState::Explore)
= }
= }
= }
@@ -135,10 +135,10 @@ pub fn preload_assets(
=
=pub fn switch_states(
= date: Res<Date>,
- mut state: ResMut<NextState<GameState>>,
+ mut state: ResMut<NextState<MajorState>>,
= simulation: Res<SimulationParameters>,
=) {
= if date.0 > simulation.end() {
- state.set(GameState::Explore)
+ state.set(MajorState::Explore)
= }
=}index 2bef918..bf395a9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -17,7 +17,7 @@ use otterhide::roads::RoadsPlugin;
=use otterhide::simulation::SimulationLoadingPlugin;
=use otterhide::sun::SunPlugin;
=use otterhide::switch_states;
-use otterhide::GameState;
+use otterhide::MajorState;
=use otterhide::PreloadedAssets;
=use otterhide::SimulationParameters;
=
@@ -36,7 +36,7 @@ fn main() {
= .register_type::<SimulationParameters>()
= .init_resource::<SimulationParameters>()
= .init_resource::<PreloadedAssets>()
- .init_state::<GameState>()
+ .init_state::<MajorState>()
= .add_plugins(ConfigurationPlugin)
= .add_plugins(SimulationLoadingPlugin)
= .add_plugins(CameraPlugin)
@@ -52,7 +52,7 @@ fn main() {
= .add_plugins(HudPlugin)
= .add_plugins(PopulationPlugin)
= .add_systems(Startup, greet)
- .add_systems(Update, preload_assets.run_if(in_state(GameState::Loading)))
+ .add_systems(Update, preload_assets.run_if(in_state(MajorState::Loading)))
= .add_systems(Update, switch_states.run_if(on_event::<NewMonth>()))
= .run()
=}index 1ab95fa..2201375 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -3,7 +3,7 @@ use crate::date;
=use crate::date::SimulationFrameCounter;
=use crate::history::Snapshot;
=use crate::names::{FEMALE_NAMES, LAST_NAMES, MALE_NAMES};
-use crate::GameState;
+use crate::MajorState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy::utils::{HashMap, Uuid};
@@ -31,7 +31,7 @@ impl Plugin for PopulationPlugin {
= .add_systems(
= Update,
= plan_immigration.before(implement_immigration).run_if(
- in_state(GameState::Simulate)
+ in_state(MajorState::Simulate)
= .and_then(resource_changed::<SimulationFrameCounter>)
= .and_then(date::past_hour(6.0)),
= ),
@@ -45,7 +45,7 @@ impl Plugin for PopulationPlugin {
= .add_systems(
= Update,
= search_for_houses.before(move_into_houses).run_if(
- in_state(GameState::Simulate)
+ in_state(MajorState::Simulate)
= .and_then(resource_changed::<SimulationFrameCounter>),
= ),
= )index 5638035..d84ccb9 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,7 +1,7 @@
=use crate::coordinates::{Coordinates, Direction};
=
=use crate::history::Snapshot;
-use crate::{history, GameState, PreloadedAssets, SimulationParameters};
+use crate::{history, MajorState, PreloadedAssets, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -21,7 +21,7 @@ impl Plugin for RoadsPlugin {
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_road_systems)
= .add_systems(
- OnEnter(GameState::Simulate),
+ OnEnter(MajorState::Simulate),
= // TODO: Avoid coupling with history systems
= lay_initial_roads.before(history::take_snapshot),
= );Separate major state and preloading to own module
On by
There is also a MajorStatePlugin defined there, responsible for switching states. I want to rework the logic around switching states, but for now I just collected existing code and crammed it in this new module.
index bee15b8..33a2444 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -1,9 +1,10 @@
=use crate::date::SimulationFrameCounter;
=use crate::districts::District;
=use crate::history::Snapshot;
+use crate::major_state::MajorState;
+use crate::major_state::PreloadedAssets;
=use crate::parcels::{Parcel, ParcelNumber};
=use crate::population::{MovedIn, Person, PersonId};
-use crate::{MajorState, PreloadedAssets};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;index 3aa78d9..3dcbded 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -1,6 +1,7 @@
=use crate::day_month::{DayMonth, HOUR, MINUTE};
=use crate::history::Snapshot;
-use crate::{MajorState, SimulationParameters};
+use crate::major_state::MajorState;
+use crate::SimulationParameters;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;index 16b4cfe..fab925b 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -2,11 +2,13 @@ use crate::buildings::Building;
=use crate::coordinates::{Coordinate, Coordinates, Direction, Latitude, Longitude};
=use crate::date::SimulationFrameCounter;
=use crate::history::Snapshot;
+use crate::major_state::MajorState;
+use crate::major_state::PreloadedAssets;
=use crate::names::{DISTRICT_NAMES, DISTRICT_PREFIXES};
=use crate::parcels::{setup_parcels, Parcel};
=use crate::population::Person;
=use crate::roads::{RoadPlan, RoadsSystems};
-use crate::{MajorState, PreloadedAssets, SimulationParameters};
+use crate::SimulationParameters;
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;index 22d190b..af216a9 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -4,6 +4,7 @@ use crate::configuration::Configuration;
=use crate::date::{Date, DateSystems};
=use crate::history::History;
=use crate::history::HistorySystems;
+use crate::major_state::MajorState;
=use crate::population::Person;
=use crate::population::PersonId;
=use crate::population::PersonsRegister;
@@ -11,7 +12,6 @@ use crate::population::Residence;
=use crate::population::Savings;
=use crate::population::Sex;
=use crate::ron_export;
-use crate::MajorState;
=use crate::SimulationParameters;
=use bevy::prelude::*;
=use bevy_egui::egui;index 045f82d..7574695 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -3,9 +3,9 @@ use crate::date::{Date, DateSnapshot, NewMonth};
=use crate::day_month::{DayMonth, Month};
=use crate::districts::DistrictsSnapshot;
=use crate::districts::NewDistrictEstablished;
+use crate::major_state::MajorState;
=use crate::population::{Immigrated, MovedIn, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
-use crate::MajorState;
=use bevy::ecs::system::{RunSystemOnce, SystemId};
=use bevy::prelude::*;
=use serde::{Deserialize, Serialize};index 454dcb6..c9c8b2e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -8,6 +8,7 @@ pub mod districts;
=pub mod ground;
=pub mod heads_up_display;
=pub mod history;
+pub mod major_state;
=pub mod names;
=pub mod parcels;
=pub mod pgsql_export;
@@ -18,16 +19,11 @@ pub mod simulation;
=pub mod sun;
=
=use bevy::prelude::*;
-use bevy::utils::HashSet;
=use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
=use bevy_inspector_egui::prelude::*;
-use date::Date;
=use day_month::DayMonth;
=use day_month::HOUR;
-use history::History;
-use history::HistorySystems;
=pub use simulation::Simulation;
-use simulation::SimulationAssetHandle;
=
=// TODO: Use configuration to set SimulationParameters.
=// Store them in and restore from Simulation asset.
@@ -83,62 +79,3 @@ impl SimulationParameters {
= *DayMonth::new(self.first_year + self.years as i32).advance(self.frame_offset)
= }
=}
-
-#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
-pub enum MajorState {
- #[default]
- Loading,
- Simulate,
- Explore,
-}
-
-#[derive(Resource, Default, Debug)]
-pub struct PreloadedAssets(HashSet<UntypedHandle>);
-
-// TODO: The preload_assets function has too many responsibilities. Refactor.
-pub fn preload_assets(
- server: Res<AssetServer>,
- mut state: ResMut<NextState<MajorState>>,
- assets: Res<PreloadedAssets>,
- mut history: ResMut<History>,
- loaded: Option<Res<SimulationAssetHandle>>,
- simulations: Res<Assets<Simulation>>,
- history_systems: Res<HistorySystems>,
- mut commands: Commands,
-) {
- let count = assets.0.len();
-
- info!("Waiting for {count} assets to preload",);
- let mut ids = assets.0.iter().map(|handle| handle.id());
- if ids.all(|id| server.is_loaded_with_dependencies(id)) {
- info!("All {count} assets preloaded!");
-
- match loaded {
- None => state.set(MajorState::Simulate),
-
- Some(simulation) => {
- let simulation = simulations.get(simulation.0.clone()).unwrap();
-
- info!(
- "Loaded the simulation asset with {} snapshots and {} events",
- simulation.history.snapshots.len(),
- simulation.history.events.len()
- );
- *history = simulation.history.clone();
- let snapshot = history.snapshots[0].clone();
- commands.run_system_with_input(history_systems.rollback, snapshot);
- state.set(MajorState::Explore)
- }
- }
- }
-}
-
-pub fn switch_states(
- date: Res<Date>,
- mut state: ResMut<NextState<MajorState>>,
- simulation: Res<SimulationParameters>,
-) {
- if date.0 > simulation.end() {
- state.set(MajorState::Explore)
- }
-}index bf395a9..89bf061 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,20 +5,16 @@ use otterhide::buildings::BuildingsPlugin;
=use otterhide::camera::CameraPlugin;
=use otterhide::configuration::ConfigurationPlugin;
=use otterhide::date::DatePlugin;
-use otterhide::date::NewMonth;
=use otterhide::districts::DistrictsPlugin;
=use otterhide::ground::GroundPlugin;
=use otterhide::heads_up_display::HudPlugin;
=use otterhide::history::HistoryPlugin;
+use otterhide::major_state::MajorStatePlugin;
=use otterhide::parcels::ParcelsPlugin;
=use otterhide::population::PopulationPlugin;
-use otterhide::preload_assets;
=use otterhide::roads::RoadsPlugin;
=use otterhide::simulation::SimulationLoadingPlugin;
=use otterhide::sun::SunPlugin;
-use otterhide::switch_states;
-use otterhide::MajorState;
-use otterhide::PreloadedAssets;
=use otterhide::SimulationParameters;
=
=fn main() {
@@ -35,9 +31,8 @@ fn main() {
= .register_type::<Uuid>()
= .register_type::<SimulationParameters>()
= .init_resource::<SimulationParameters>()
- .init_resource::<PreloadedAssets>()
- .init_state::<MajorState>()
= .add_plugins(ConfigurationPlugin)
+ .add_plugins(MajorStatePlugin)
= .add_plugins(SimulationLoadingPlugin)
= .add_plugins(CameraPlugin)
= .add_plugins(GroundPlugin)
@@ -52,8 +47,6 @@ fn main() {
= .add_plugins(HudPlugin)
= .add_plugins(PopulationPlugin)
= .add_systems(Startup, greet)
- .add_systems(Update, preload_assets.run_if(in_state(MajorState::Loading)))
- .add_systems(Update, switch_states.run_if(on_event::<NewMonth>()))
= .run()
=}
=new file mode 100644
index 0000000..a7f359d
--- /dev/null
+++ b/src/major_state.rs
@@ -0,0 +1,79 @@
+use bevy::{prelude::*, utils::HashSet};
+
+use crate::{
+ date::{Date, NewMonth},
+ history::{History, HistorySystems},
+ simulation::SimulationAssetHandle,
+ Simulation, SimulationParameters,
+};
+
+/// The primary state of the program
+#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
+pub enum MajorState {
+ #[default]
+ Loading,
+ Simulate,
+ Explore,
+}
+
+pub struct MajorStatePlugin;
+
+impl Plugin for MajorStatePlugin {
+ fn build(&self, app: &mut App) {
+ app.init_state::<MajorState>()
+ .init_resource::<PreloadedAssets>()
+ .add_systems(Update, preload_assets.run_if(in_state(MajorState::Loading)))
+ .add_systems(Update, switch_states.run_if(on_event::<NewMonth>()));
+ }
+}
+
+#[derive(Resource, Default, Debug)]
+pub struct PreloadedAssets(pub HashSet<UntypedHandle>);
+
+// TODO: The preload_assets function has too many responsibilities. Refactor.
+pub fn preload_assets(
+ server: Res<AssetServer>,
+ mut state: ResMut<NextState<MajorState>>,
+ assets: Res<PreloadedAssets>,
+ mut history: ResMut<History>,
+ loaded: Option<Res<SimulationAssetHandle>>,
+ simulations: Res<Assets<Simulation>>,
+ history_systems: Res<HistorySystems>,
+ mut commands: Commands,
+) {
+ let count = assets.0.len();
+
+ info!("Waiting for {count} assets to preload",);
+ let mut ids = assets.0.iter().map(|handle| handle.id());
+ if ids.all(|id| server.is_loaded_with_dependencies(id)) {
+ info!("All {count} assets preloaded!");
+
+ match loaded {
+ None => state.set(MajorState::Simulate),
+
+ Some(simulation) => {
+ let simulation = simulations.get(simulation.0.clone()).unwrap();
+
+ info!(
+ "Loaded the simulation asset with {} snapshots and {} events",
+ simulation.history.snapshots.len(),
+ simulation.history.events.len()
+ );
+ *history = simulation.history.clone();
+ let snapshot = history.snapshots[0].clone();
+ commands.run_system_with_input(history_systems.rollback, snapshot);
+ state.set(MajorState::Explore)
+ }
+ }
+ }
+}
+
+pub fn switch_states(
+ date: Res<Date>,
+ mut state: ResMut<NextState<MajorState>>,
+ simulation: Res<SimulationParameters>,
+) {
+ if date.0 > simulation.end() {
+ state.set(MajorState::Explore)
+ }
+}index 2201375..6f63ae4 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -2,8 +2,8 @@ use crate::buildings::{BuildingId, HasSpareCapacity};
=use crate::date;
=use crate::date::SimulationFrameCounter;
=use crate::history::Snapshot;
+use crate::major_state::MajorState;
=use crate::names::{FEMALE_NAMES, LAST_NAMES, MALE_NAMES};
-use crate::MajorState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy::utils::{HashMap, Uuid};index d84ccb9..21f46d4 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,7 +1,10 @@
=use crate::coordinates::{Coordinates, Direction};
=
+use crate::history;
=use crate::history::Snapshot;
-use crate::{history, MajorState, PreloadedAssets, SimulationParameters};
+use crate::major_state::MajorState;
+use crate::major_state::PreloadedAssets;
+use crate::SimulationParameters;
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;index a41d203..f212189 100644
--- a/src/simulation.rs
+++ b/src/simulation.rs
@@ -2,8 +2,9 @@ use bevy::asset::AsyncReadExt;
=use bevy::{asset::AssetLoader, prelude::*};
=use serde::{Deserialize, Serialize};
=use thiserror::Error;
-
-use crate::{configuration::Configuration, history::History, PreloadedAssets};
+use crate::history::History;
+use crate::configuration::Configuration;
+use crate::major_state::PreloadedAssets;
=
=pub struct SimulationLoadingPlugin;
=Use gerunds instead of verbs to name states
On by
The Loading state was already a gerund (aka the -ing form), but Simulate and Explore were verbs. That was inconsistent and generally sounded off.
index 33a2444..496fff8 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -39,7 +39,7 @@ impl Plugin for BuildingsPlugin {
= .add_systems(
= Update,
= order_construction.run_if(
- in_state(MajorState::Simulate)
+ in_state(MajorState::Simulating)
= .and_then(resource_changed::<SimulationFrameCounter>),
= ),
= )index 3dcbded..e915e0d 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -25,7 +25,7 @@ impl Plugin for DatePlugin {
= .add_systems(Startup, register_date_systems)
= .add_systems(
= PreUpdate,
- advance_exploration_date.run_if(in_state(MajorState::Explore)),
+ advance_exploration_date.run_if(in_state(MajorState::Exploring)),
= )
= .add_systems(PreUpdate, update_date_display);
= }index fab925b..cb646bd 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -37,7 +37,7 @@ impl Plugin for DistrictsPlugin {
= .add_systems(
= Update,
= plan_new_districts.after(implement_new_districts).run_if(
- in_state(MajorState::Simulate)
+ in_state(MajorState::Simulating)
= .and_then(resource_changed::<SimulationFrameCounter>),
= ),
= )index af216a9..66ea5a9 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -56,8 +56,8 @@ impl Plugin for HudPlugin {
= .add_systems(
= PostUpdate,
= (
- simulation_system_set.run_if(in_state(MajorState::Simulate)),
- exploration_system_set.run_if(in_state(MajorState::Explore)),
+ simulation_system_set.run_if(in_state(MajorState::Simulating)),
+ exploration_system_set.run_if(in_state(MajorState::Exploring)),
= central_panel_system_set,
= )
= .chain(),index 7574695..7638858 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -19,19 +19,19 @@ impl Plugin for HistoryPlugin {
= app.init_resource::<History>()
= .init_resource::<Future>()
= .add_systems(Startup, setup_history_systems)
- .add_systems(OnEnter(MajorState::Simulate), take_snapshot)
+ .add_systems(OnEnter(MajorState::Simulating), take_snapshot)
= .add_systems(
= PreUpdate,
= take_snapshot
- .run_if(in_state(MajorState::Simulate).and_then(on_event::<NewMonth>())),
+ .run_if(in_state(MajorState::Simulating).and_then(on_event::<NewMonth>())),
= )
= .add_systems(
= Update,
- register_historical_events.run_if(in_state(MajorState::Simulate)),
+ register_historical_events.run_if(in_state(MajorState::Simulating)),
= )
= .add_systems(
= Update,
- replay_historical_events.run_if(in_state(MajorState::Explore)),
+ replay_historical_events.run_if(in_state(MajorState::Exploring)),
= );
= }
=}
@@ -188,7 +188,7 @@ fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= world.run_system_once(clear_pending_historical_events);
=
= world.resource_scope(|_, mut game_state: Mut<NextState<MajorState>>| {
- game_state.set(MajorState::Explore);
+ game_state.set(MajorState::Exploring);
= });
=
= // TODO: DRY on rollback. Maybe a macro?index a7f359d..4917516 100644
--- a/src/major_state.rs
+++ b/src/major_state.rs
@@ -12,8 +12,8 @@ use crate::{
=pub enum MajorState {
= #[default]
= Loading,
- Simulate,
- Explore,
+ Simulating,
+ Exploring,
=}
=
=pub struct MajorStatePlugin;
@@ -49,7 +49,7 @@ pub fn preload_assets(
= info!("All {count} assets preloaded!");
=
= match loaded {
- None => state.set(MajorState::Simulate),
+ None => state.set(MajorState::Simulating),
=
= Some(simulation) => {
= let simulation = simulations.get(simulation.0.clone()).unwrap();
@@ -62,18 +62,19 @@ pub fn preload_assets(
= *history = simulation.history.clone();
= let snapshot = history.snapshots[0].clone();
= commands.run_system_with_input(history_systems.rollback, snapshot);
- state.set(MajorState::Explore)
+ state.set(MajorState::Exploring)
= }
= }
= }
=}
=
+// TODO: Rename switch_states - it only switches from simulate to explore
=pub fn switch_states(
= date: Res<Date>,
= mut state: ResMut<NextState<MajorState>>,
= simulation: Res<SimulationParameters>,
=) {
= if date.0 > simulation.end() {
- state.set(MajorState::Explore)
+ state.set(MajorState::Exploring)
= }
=}index 6f63ae4..057615a 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -31,7 +31,7 @@ impl Plugin for PopulationPlugin {
= .add_systems(
= Update,
= plan_immigration.before(implement_immigration).run_if(
- in_state(MajorState::Simulate)
+ in_state(MajorState::Simulating)
= .and_then(resource_changed::<SimulationFrameCounter>)
= .and_then(date::past_hour(6.0)),
= ),
@@ -45,7 +45,7 @@ impl Plugin for PopulationPlugin {
= .add_systems(
= Update,
= search_for_houses.before(move_into_houses).run_if(
- in_state(MajorState::Simulate)
+ in_state(MajorState::Simulating)
= .and_then(resource_changed::<SimulationFrameCounter>),
= ),
= )index 21f46d4..1452cef 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -24,7 +24,7 @@ impl Plugin for RoadsPlugin {
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_road_systems)
= .add_systems(
- OnEnter(MajorState::Simulate),
+ OnEnter(MajorState::Simulating),
= // TODO: Avoid coupling with history systems
= lay_initial_roads.before(history::take_snapshot),
= );Fix: first snapshot restored before districts setup
On by
For this I introduced a new major state: getting ready. Currently it only lasts one frame, when districts and buildings assets are processed.
This setup paves the way for removing snapshots from the saved simulation, to be restored when it's loaded again (and hopefully getting file sizes from tens of gigabytes to tens of megabytes).
index 496fff8..294925b 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -35,7 +35,7 @@ impl Plugin for BuildingsPlugin {
= .register_type::<BuildingId>()
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_buildings_systems)
- .add_systems(OnExit(MajorState::Loading), setup_building_scenes)
+ .add_systems(OnEnter(MajorState::GettingReady), setup_building_scenes)
= .add_systems(
= Update,
= order_construction.run_if(index f720333..161c7c3 100644
--- a/src/configuration.rs
+++ b/src/configuration.rs
@@ -8,7 +8,7 @@ impl Plugin for ConfigurationPlugin {
= fn build(&self, app: &mut App) {
= app.register_type::<Configuration>()
= .add_plugins(BevyArgsPlugin::<Configuration>::default())
- .add_systems(Startup, print_configuration);
+ .add_systems(PreStartup, print_configuration);
= }
=}
=index cb646bd..5e37ff1 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -32,8 +32,7 @@ impl Plugin for DistrictsPlugin {
= .init_resource::<DistrictScenes>()
= .add_systems(Startup, register_district_systems)
= .add_systems(Startup, setup_assets)
- // FIXME: The setup_districts system runs after the first snapshot is restored!
- .add_systems(OnExit(MajorState::Loading), setup_districts)
+ .add_systems(OnEnter(MajorState::GettingReady), setup_districts)
= .add_systems(
= Update,
= plan_new_districts.after(implement_new_districts).run_if(
@@ -41,6 +40,7 @@ impl Plugin for DistrictsPlugin {
= .and_then(resource_changed::<SimulationFrameCounter>),
= ),
= )
+ // TODO: Not in the getting ready state
= .add_systems(Update, implement_new_districts);
= }
=}index 4917516..dcfc919 100644
--- a/src/major_state.rs
+++ b/src/major_state.rs
@@ -1,29 +1,58 @@
-use bevy::{prelude::*, utils::HashSet};
-
-use crate::{
- date::{Date, NewMonth},
- history::{History, HistorySystems},
- simulation::SimulationAssetHandle,
- Simulation, SimulationParameters,
-};
+use crate::date::{Date, NewMonth};
+use crate::simulation::SimulationAssetHandle;
+use crate::SimulationParameters;
+use bevy::ecs::system::SystemId;
+use bevy::prelude::*;
+use bevy::utils::HashSet;
+use std::ops::Not;
=
=/// The primary state of the program
=#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
=pub enum MajorState {
+ /// While all the assets are being loaded
+ ///
+ /// Use the [PreloadedAssets][] resource to make the program wait for
+ /// selected assets being fully loaded before switchting to [the Processing
+ /// state](MajorState::Processing).
= #[default]
= Loading,
+
+ /// While the assets are being processed
+ ///
+ /// If getting a particular asset can be done in one synchronous step, do it
+ /// by adding a system in the `OnEnter(MajorStates::GettingReady)` schedule.
+ /// Otherwise (like in the case of Simulation), use the [ReadyPredicates][]
+ /// resource to make the program wait for processing that takes more than
+ /// one frame.
+ GettingReady,
+
+ /// While the data set is being generated
= Simulating,
+
+ /// While the user journeys across space and time
= Exploring,
=}
=
+/// The plugin responsible for switching between [major states](MajorState).
=pub struct MajorStatePlugin;
=
=impl Plugin for MajorStatePlugin {
= fn build(&self, app: &mut App) {
= app.init_state::<MajorState>()
= .init_resource::<PreloadedAssets>()
- .add_systems(Update, preload_assets.run_if(in_state(MajorState::Loading)))
- .add_systems(Update, switch_states.run_if(on_event::<NewMonth>()));
+ .init_resource::<ReadyPredicates>()
+ .add_systems(
+ PostUpdate,
+ exit_loading_state.run_if(in_state(MajorState::Loading)),
+ )
+ .add_systems(
+ PostUpdate,
+ exit_getting_ready_state.run_if(in_state(MajorState::GettingReady)),
+ )
+ .add_systems(
+ PostUpdate,
+ exit_simulating_state.run_if(on_event::<NewMonth>()),
+ );
= }
=}
=
@@ -31,50 +60,74 @@ impl Plugin for MajorStatePlugin {
=pub struct PreloadedAssets(pub HashSet<UntypedHandle>);
=
=// TODO: The preload_assets function has too many responsibilities. Refactor.
-pub fn preload_assets(
+/// This system controls when the program exits Loading state
+fn exit_loading_state(
= server: Res<AssetServer>,
= mut state: ResMut<NextState<MajorState>>,
+ current_state: Res<State<MajorState>>,
= assets: Res<PreloadedAssets>,
- mut history: ResMut<History>,
- loaded: Option<Res<SimulationAssetHandle>>,
- simulations: Res<Assets<Simulation>>,
- history_systems: Res<HistorySystems>,
- mut commands: Commands,
=) {
- let count = assets.0.len();
-
- info!("Waiting for {count} assets to preload",);
- let mut ids = assets.0.iter().map(|handle| handle.id());
- if ids.all(|id| server.is_loaded_with_dependencies(id)) {
- info!("All {count} assets preloaded!");
-
- match loaded {
- None => state.set(MajorState::Simulating),
-
- Some(simulation) => {
- let simulation = simulations.get(simulation.0.clone()).unwrap();
-
- info!(
- "Loaded the simulation asset with {} snapshots and {} events",
- simulation.history.snapshots.len(),
- simulation.history.events.len()
- );
- *history = simulation.history.clone();
- let snapshot = history.snapshots[0].clone();
- commands.run_system_with_input(history_systems.rollback, snapshot);
- state.set(MajorState::Exploring)
- }
- }
+ let total_count = assets.0.len();
+ let pending_count = assets
+ .0
+ .iter()
+ .map(|handle| handle.id())
+ .filter(|id| server.is_loaded_with_dependencies(*id).not())
+ .count();
+
+ let current_state = current_state.get();
+ let next_state = MajorState::GettingReady;
+
+ if pending_count == 0 {
+ info!("All assets loaded. Switching from {current_state:?} to {next_state:?}");
+ state.set(next_state);
+ } else {
+ info!("Waiting for {pending_count} of {total_count} assets to preload",);
= }
=}
=
-// TODO: Rename switch_states - it only switches from simulate to explore
-pub fn switch_states(
+/// A collection of predicates that indicate whether a certain subsystem is ready
+///
+/// If all predicates return true, the major state will exit the GetingReady state.
+#[derive(Resource, Default)]
+pub struct ReadyPredicates(pub Vec<SystemId<(), bool>>);
+
+fn exit_getting_ready_state(world: &mut World) {
+ let predicates =
+ world.resource_scope(|_, predicates: Mut<ReadyPredicates>| predicates.0.clone());
+ let current_state = world.resource_scope(|_, state: Mut<State<MajorState>>| *state.get());
+ let is_simulation_loaded = world.get_resource::<SimulationAssetHandle>().is_some();
+ let next_state = if is_simulation_loaded {
+ MajorState::Exploring
+ } else {
+ MajorState::Simulating
+ };
+
+ let total_count = predicates.len();
+ let pending_count = predicates
+ .iter()
+ .filter(|predicate| world.run_system(**predicate).unwrap().not())
+ .count();
+
+ if pending_count == 0 {
+ info!("All {total_count} subsystem(s) ready. Switching from {current_state:?} to {next_state:?}");
+ world.resource_scope(|_, mut state: Mut<NextState<MajorState>>| state.set(next_state));
+ } else {
+ info!("Waiting for {pending_count} of {total_count} subsystem(s) to get ready",);
+ }
+}
+
+pub fn exit_simulating_state(
= date: Res<Date>,
= mut state: ResMut<NextState<MajorState>>,
+ current_state: Res<State<MajorState>>,
= simulation: Res<SimulationParameters>,
=) {
+ let current_state = current_state.get();
+ let next_state = MajorState::Exploring;
+
= if date.0 > simulation.end() {
- state.set(MajorState::Exploring)
+ info!("Simulation complete. Switching from {current_state:?} to {next_state:?}");
+ state.set(next_state);
= }
=}index f212189..19f78f5 100644
--- a/src/simulation.rs
+++ b/src/simulation.rs
@@ -1,10 +1,10 @@
+use crate::configuration::Configuration;
+use crate::history::{History, HistorySystems};
+use crate::major_state::{MajorState, PreloadedAssets, ReadyPredicates};
=use bevy::asset::AsyncReadExt;
=use bevy::{asset::AssetLoader, prelude::*};
=use serde::{Deserialize, Serialize};
=use thiserror::Error;
-use crate::history::History;
-use crate::configuration::Configuration;
-use crate::major_state::PreloadedAssets;
=
=pub struct SimulationLoadingPlugin;
=
@@ -12,10 +12,23 @@ impl Plugin for SimulationLoadingPlugin {
= fn build(&self, app: &mut App) {
= app.init_asset::<Simulation>()
= .init_asset_loader::<SimulationLoader>()
+ .add_systems(OnEnter(MajorState::GettingReady), register_ready_predicates)
+ .add_systems(OnEnter(MajorState::GettingReady), setup_simulation)
= .add_systems(Startup, load_simulation);
= }
=}
=
+fn register_ready_predicates(world: &mut World) {
+ let predicate = world.register_system(simulation_ready);
+ world.resource_scope(|_, mut predicates: Mut<ReadyPredicates>| predicates.0.push(predicate));
+}
+
+fn simulation_ready() -> bool {
+ // TODO: If simulation was loaded, check if all snapshots are recreated
+ info!("Reporting that simulation is ready");
+ true
+}
+
=/// An asset storing a simulation that can be replayed and explored
=#[derive(Asset, Debug, Deserialize, Serialize, TypePath)]
=pub struct Simulation {
@@ -27,7 +40,7 @@ fn load_simulation(
= configuration: Res<Configuration>,
= assets: Res<AssetServer>,
= mut preloaded: ResMut<PreloadedAssets>,
- mut commands: Commands
+ mut commands: Commands,
=) {
= // Only load the asset if CLI / URL parameters ask for it
= if let Some(path) = &configuration.load {
@@ -37,14 +50,36 @@ fn load_simulation(
= }
=}
=
+pub fn setup_simulation(
+ handle: Option<Res<SimulationAssetHandle>>,
+ assets: Res<Assets<Simulation>>,
+ mut history: ResMut<History>,
+ history_systems: Res<HistorySystems>,
+ mut commands: Commands,
+) {
+ if let Some(handle) = handle {
+ let simulation = assets.get(handle.0.clone()).unwrap();
+
+ // TODO: Start snapshots recalculation here
+ info!(
+ "Loaded the simulation asset with {} snapshots and {} events",
+ simulation.history.snapshots.len(),
+ simulation.history.events.len()
+ );
+ *history = simulation.history.clone();
+ let snapshot = history.snapshots[0].clone();
+ commands.run_system_with_input(history_systems.rollback, snapshot);
+ // TODO: Register the ready_predicate for history
+ }
+}
+
=#[derive(Resource)]
=pub struct SimulationAssetHandle(pub Handle<Simulation>);
=
-
=#[derive(Default)]
=struct SimulationLoader;
=impl AssetLoader for SimulationLoader {
- type Asset = Simulation ;
+ type Asset = Simulation;
=
= type Settings = ();
=
@@ -62,7 +97,7 @@ impl AssetLoader for SimulationLoader {
= reader.read_to_end(&mut content).await?;
= let history: History = ron::de::from_bytes(&content)?;
= // let history = 42;
- Ok(Simulation {history})
+ Ok(Simulation { history })
= })
= }
=Rein in the saved simulation file size
On by
By excluding snapshots from History we got from 22 GB to 33 MB for a 150 years long simulation. This is achieved at the expense of having to recalculate the snapshots after loading the simulation. This is currently slow. Takes about as much time as running a fresh simulation. On my laptop it was about 10 minutes. That's not good, but certainly better than having to transfer tens of gigabytes. Also, running this application consumes about 10 GB of RAM. Not good either.
Anyways, this is a big win that should enable us to ship an MVP and improve things later. I have few ideas for possible improvements.
-
Calm down logging
Currently the app loads insane amounts of data and it might slow it down. Most of the log sources should be downgraded to debug level.
-
Disable camera in GettingReady state
That is when the snapshots are recalculated. Perhaps not rendering the world will speed things up. On the other hand, seeing things taking shape might ease up the waiting time for students. So it's a tradeoff.
-
Storing snapshots as separate assets that can be loaded on demand
Requires some design thinking regarding keeping the simulation (events) and associated snapshots in sync. Size remains a problem too, even if it's split between multiple files.
-
Storing snapshots in temporary files / session storage
This would address the memory consumption problem, and indirectly perhaps speed too.
index e915e0d..b541b35 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -1,5 +1,5 @@
=use crate::day_month::{DayMonth, HOUR, MINUTE};
-use crate::history::Snapshot;
+use crate::history::{Future, Snapshot};
=use crate::major_state::MajorState;
=use crate::SimulationParameters;
=use bevy::ecs::system::SystemId;
@@ -23,6 +23,10 @@ impl Plugin for DatePlugin {
= .add_event::<NewMonth>()
= .add_systems(Startup, setup_date_display)
= .add_systems(Startup, register_date_systems)
+ .add_systems(
+ PreUpdate,
+ advance_getting_ready_date.run_if(in_state(MajorState::GettingReady)),
+ )
= .add_systems(
= PreUpdate,
= advance_exploration_date.run_if(in_state(MajorState::Exploring)),
@@ -106,6 +110,20 @@ fn advance_exploration_date(
= date.0.advance(duration);
=}
=
+/// Advances the date to the first event in the future
+fn advance_getting_ready_date(
+ mut date: ResMut<Date>,
+ future: Res<Future>,
+ mut new_month: EventWriter<NewMonth>,
+) {
+ if let Some(entry) = future.events.first() {
+ if entry.date.month() != date.0.month() {
+ new_month.send(NewMonth);
+ }
+ date.0 = entry.date;
+ }
+}
+
=// TODO: Consider if date display logic should go to a UI module?
=#[derive(Component)]
=struct DateDisplay;index 66ea5a9..f1ed61e 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -4,6 +4,7 @@ use crate::configuration::Configuration;
=use crate::date::{Date, DateSystems};
=use crate::history::History;
=use crate::history::HistorySystems;
+use crate::history::Snapshots;
=use crate::major_state::MajorState;
=use crate::population::Person;
=use crate::population::PersonId;
@@ -161,6 +162,7 @@ fn paint_simulation_progress_bar(
=fn paint_replay_controls(
= mut contexts: EguiContexts,
= history: Res<History>,
+ snapshots: Res<Snapshots>,
= mut slider_value: ResMut<DateSliderValue>,
= mut time: ResMut<Time<Virtual>>,
=) {
@@ -223,7 +225,7 @@ fn paint_replay_controls(
=
= ui.end_row();
=
- let snapshots_count = (history.snapshots.len() - 1) as f64;
+ let snapshots_count = (snapshots.collection.len() - 1) as f64;
= let range = 0.0..=snapshots_count;
=
= let slider = Slider::from_get_set(range, |input| {
@@ -254,16 +256,16 @@ fn update_slider_value(
=
=fn apply_slider_value(
= slider_value: ResMut<DateSliderValue>,
- history: Res<History>,
+ snapshots: Res<Snapshots>,
= systems: Res<HistorySystems>,
= mut commands: Commands,
=) {
= let index = slider_value.0 as usize;
= info!(
= "Loading snapshot {index} out of {count}",
- count = history.snapshots.len()
+ count = snapshots.collection.len()
= );
- let Some(snapshot) = history.snapshots.get(index) else {
+ let Some(snapshot) = snapshots.collection.get(index) else {
= warn!("No snapshot at index {index}!");
= return;
= };index 7638858..f2d1f06 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -3,14 +3,15 @@ use crate::date::{Date, DateSnapshot, NewMonth};
=use crate::day_month::{DayMonth, Month};
=use crate::districts::DistrictsSnapshot;
=use crate::districts::NewDistrictEstablished;
-use crate::major_state::MajorState;
+use crate::major_state::{MajorState, ReadyPredicates};
=use crate::population::{Immigrated, MovedIn, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
+use crate::simulation::SimulationAssetHandle;
+use crate::Simulation;
=use bevy::ecs::system::{RunSystemOnce, SystemId};
=use bevy::prelude::*;
=use serde::{Deserialize, Serialize};
=use std::fmt::Display;
-use std::ops::DerefMut;
=
=pub struct HistoryPlugin;
=
@@ -18,28 +19,68 @@ impl Plugin for HistoryPlugin {
= fn build(&self, app: &mut App) {
= app.init_resource::<History>()
= .init_resource::<Future>()
+ .init_resource::<Snapshots>()
= .add_systems(Startup, setup_history_systems)
- .add_systems(OnEnter(MajorState::Simulating), take_snapshot)
+ .add_systems(Startup, register_ready_predicates)
+ .add_systems(OnEnter(MajorState::GettingReady), import_simulation)
= .add_systems(
= PreUpdate,
- take_snapshot
- .run_if(in_state(MajorState::Simulating).and_then(on_event::<NewMonth>())),
+ take_snapshot.run_if(
+ in_state(MajorState::GettingReady)
+ .or_else(in_state(MajorState::Simulating))
+ .and_then(on_event::<NewMonth>()),
+ ),
= )
= .add_systems(
- Update,
- register_historical_events.run_if(in_state(MajorState::Simulating)),
+ PostUpdate,
+ register_historical_events.run_if(
+ in_state(MajorState::Simulating).or_else(in_state(MajorState::GettingReady)),
+ ),
= )
= .add_systems(
- Update,
- replay_historical_events.run_if(in_state(MajorState::Exploring)),
+ PreUpdate,
+ replay_historical_events.run_if(
+ in_state(MajorState::GettingReady).or_else(in_state(MajorState::Exploring)),
+ ),
= );
= }
=}
=
+fn register_ready_predicates(world: &mut World) {
+ let predicate = world.register_system(history_ready);
+ world.resource_scope(|_, mut predicates: Mut<ReadyPredicates>| predicates.0.push(predicate));
+}
+
+fn history_ready(future: Res<Future>) -> bool {
+ future.events.is_empty()
+}
+
+pub fn import_simulation(
+ handle: Option<Res<SimulationAssetHandle>>,
+ assets: Res<Assets<Simulation>>,
+ mut future: ResMut<Future>,
+) {
+ if let Some(handle) = handle {
+ let simulation = assets.get(handle.0.clone()).unwrap();
+
+ // TODO: Start snapshots recalculation here
+ info!(
+ "Loaded the simulation asset with {} events",
+ simulation.events.len()
+ );
+ // TODO: Don't clone the events from loaded simulation - move them!
+ future.events = simulation.events.clone();
+ }
+}
+
=#[derive(Resource, Default, Clone, Debug, Serialize, Deserialize)]
=pub struct History {
= pub events: Vec<EventLogEntry>,
- pub snapshots: Vec<MainSnapshot>,
+}
+
+#[derive(Resource, Default, Clone, Debug, Serialize, Deserialize)]
+pub struct Snapshots {
+ pub collection: Vec<MainSnapshot>,
=}
=
=/// A collection of events that will happen in the future from the time traveler's perspective
@@ -47,7 +88,7 @@ pub struct History {
=/// Reset by time_travel. Used by reply_historical_events.
=#[derive(Resource, Default, Clone)]
=pub struct Future {
- events: Vec<EventLogEntry>,
+ pub events: Vec<EventLogEntry>,
=}
=
=// TODO: If the Snapshot trait experiment works, move it to the history module
@@ -176,13 +217,13 @@ pub fn take_snapshot(world: &mut World) {
= population,
= };
=
- world.resource_scope(|_, mut history: Mut<History>| {
- history.deref_mut().snapshots.push(snapshot);
+ world.resource_scope(|_, mut snapshots: Mut<Snapshots>| {
+ snapshots.collection.push(snapshot);
= })
=}
=
=fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
- info!("Restoring {snapshot:#?}");
+ info!("Restoring snapshot {}", snapshot.date.0);
= // Make sure no pending events from the future are going to reach systems
= // after the rollback
= world.run_system_once(clear_pending_historical_events);
@@ -206,6 +247,12 @@ fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= .into_iter()
= .filter(|event| event.date >= snapshot.date.0)
= .collect();
+
+ info!(
+ "Copied {} of {} events from the history to the future",
+ future.events.len(),
+ events.len()
+ )
= });
=}
=index 1452cef..471ac74 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -24,7 +24,7 @@ impl Plugin for RoadsPlugin {
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_road_systems)
= .add_systems(
- OnEnter(MajorState::Simulating),
+ OnEnter(MajorState::GettingReady),
= // TODO: Avoid coupling with history systems
= lay_initial_roads.before(history::take_snapshot),
= );index 19f78f5..8039eb8 100644
--- a/src/simulation.rs
+++ b/src/simulation.rs
@@ -1,6 +1,6 @@
=use crate::configuration::Configuration;
-use crate::history::{History, HistorySystems};
-use crate::major_state::{MajorState, PreloadedAssets, ReadyPredicates};
+use crate::history::EventLogEntry;
+use crate::major_state::PreloadedAssets;
=use bevy::asset::AsyncReadExt;
=use bevy::{asset::AssetLoader, prelude::*};
=use serde::{Deserialize, Serialize};
@@ -12,28 +12,15 @@ impl Plugin for SimulationLoadingPlugin {
= fn build(&self, app: &mut App) {
= app.init_asset::<Simulation>()
= .init_asset_loader::<SimulationLoader>()
- .add_systems(OnEnter(MajorState::GettingReady), register_ready_predicates)
- .add_systems(OnEnter(MajorState::GettingReady), setup_simulation)
= .add_systems(Startup, load_simulation);
= }
=}
=
-fn register_ready_predicates(world: &mut World) {
- let predicate = world.register_system(simulation_ready);
- world.resource_scope(|_, mut predicates: Mut<ReadyPredicates>| predicates.0.push(predicate));
-}
-
-fn simulation_ready() -> bool {
- // TODO: If simulation was loaded, check if all snapshots are recreated
- info!("Reporting that simulation is ready");
- true
-}
-
=/// An asset storing a simulation that can be replayed and explored
=#[derive(Asset, Debug, Deserialize, Serialize, TypePath)]
=pub struct Simulation {
= // TODO: Save other simulation parameters, like duration
- pub history: History,
+ pub events: Vec<EventLogEntry>,
=}
=
=fn load_simulation(
@@ -50,29 +37,6 @@ fn load_simulation(
= }
=}
=
-pub fn setup_simulation(
- handle: Option<Res<SimulationAssetHandle>>,
- assets: Res<Assets<Simulation>>,
- mut history: ResMut<History>,
- history_systems: Res<HistorySystems>,
- mut commands: Commands,
-) {
- if let Some(handle) = handle {
- let simulation = assets.get(handle.0.clone()).unwrap();
-
- // TODO: Start snapshots recalculation here
- info!(
- "Loaded the simulation asset with {} snapshots and {} events",
- simulation.history.snapshots.len(),
- simulation.history.events.len()
- );
- *history = simulation.history.clone();
- let snapshot = history.snapshots[0].clone();
- commands.run_system_with_input(history_systems.rollback, snapshot);
- // TODO: Register the ready_predicate for history
- }
-}
-
=#[derive(Resource)]
=pub struct SimulationAssetHandle(pub Handle<Simulation>);
=
@@ -95,9 +59,8 @@ impl AssetLoader for SimulationLoader {
= // TODO: Serialize and deserialize to Simulation directly
= let mut content = Vec::new();
= reader.read_to_end(&mut content).await?;
- let history: History = ron::de::from_bytes(&content)?;
- // let history = 42;
- Ok(Simulation { history })
+ let simulation: Simulation = ron::de::from_bytes(&content)?;
+ Ok(simulation)
= })
= }
=Fix check/sqlite-export goal in the Makefile
On by
It was only exporting schema.
index c5aa041..f740736 100644
--- a/Makefile
+++ b/Makefile
@@ -56,7 +56,6 @@ check/watch:
=.PHONY: check/watch
=
=check/sqlite-export: ## Try exporting the simulation.ron file to SQL
-check/sqlite-export: check/sqlite-export/schema
=check/sqlite-export: data/sqlite-export.sql
=.PHONY: check/sqlite-export
=
@@ -91,6 +90,7 @@ data/sqlite-export.sql: data/sqlite-export.db
= sqlite3 $< .dump > $@
=
=data/sqlite-export.db: assets/simulation.ron
+data/sqlite-export.db: check/sqlite-export/schema
=data/sqlite-export.db: src/**/*
=data/sqlite-export.db:
= mkdir --parents $(@D)Implement a HUD progress bar for getting ready
On by
index f1ed61e..a4c2b9e 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -2,6 +2,7 @@ use crate::buildings::Building;
=use crate::buildings::BuildingsRegister;
=use crate::configuration::Configuration;
=use crate::date::{Date, DateSystems};
+use crate::history::Future;
=use crate::history::History;
=use crate::history::HistorySystems;
=use crate::history::Snapshots;
@@ -32,6 +33,7 @@ pub struct HudPlugin;
=
=impl Plugin for HudPlugin {
= fn build(&self, app: &mut App) {
+ let getting_ready_system_set = (paint_getting_ready_progress_bar,).chain();
= let simulation_system_set = (
= auto_advance.run_if(resource_equals(AutoAdvanceSimulation(true))),
= paint_simulation_progress_bar,
@@ -57,6 +59,7 @@ impl Plugin for HudPlugin {
= .add_systems(
= PostUpdate,
= (
+ getting_ready_system_set.run_if(in_state(MajorState::GettingReady)),
= simulation_system_set.run_if(in_state(MajorState::Simulating)),
= exploration_system_set.run_if(in_state(MajorState::Exploring)),
= central_panel_system_set,
@@ -94,6 +97,49 @@ fn auto_advance(mut commands: Commands, systems: Res<DateSystems>) {
= commands.run_system(systems.advance_simulation_date);
=}
=
+fn paint_getting_ready_progress_bar(
+ mut contexts: EguiContexts,
+ future: Res<Future>,
+ history: Res<History>,
+) {
+ egui::TopBottomPanel::bottom("getting_ready_controls")
+ .show_separator_line(false)
+ .frame(Frame {
+ inner_margin: Margin {
+ bottom: 20.,
+ top: 0.,
+ left: 40.,
+ right: 40.,
+ },
+ shadow: Shadow::NONE,
+ stroke: Stroke::NONE,
+ ..default()
+ })
+ .show(contexts.ctx_mut(), |ui| {
+ let size = ui.available_size();
+ egui::Grid::new("getting_ready_controls_grid")
+ .min_col_width(size.x)
+ .show(ui, |ui| {
+ ui.horizontal(|ui| ui.label("Processing historical events..."));
+
+ ui.end_row();
+
+ ui.horizontal(|ui| {
+ let history_events_count = history.events.len();
+ let future_events_count = future.events.len();
+ let total_events_count = history_events_count + future_events_count;
+ let progress = history_events_count as f32 / total_events_count as f32;
+ ui.add(
+ ProgressBar::new(progress)
+ .text(format!("{history_events_count} / {total_events_count}"))
+ .desired_width(size.x)
+ .desired_height(12.0),
+ );
+ });
+ });
+ });
+}
+
=fn paint_simulation_progress_bar(
= mut contexts: EguiContexts,
= date: Res<Date>,Tune down most of the logging to debug level
On by
index 294925b..8655f87 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -92,7 +92,7 @@ fn register_resident(
= buildings_register: ResMut<BuildingsRegister>,
= mut commands: Commands,
=) {
- info!("Registering resident {moved_in:#?}");
+ debug!("Registering resident {moved_in:#?}");
= let Some(building) = buildings_register.0.get(&moved_in.where_to) else {
= panic!(
= "Building {id:#?} not in the registry {buildings_register:#?}",
@@ -167,7 +167,7 @@ fn setup_building_scenes(
=) {
= let buildings = assets.get(&building_assets.0).unwrap();
= for (name, scene) in &buildings.named_scenes {
- info!("Looking for buildings in scene {name}");
+ debug!("Looking for buildings in scene {name}");
= let Ok(variant) = name.parse::<BuildingVariant>() else {
= warn!("Scene name {name} cannot be parsed as a building variant");
= continue;
@@ -336,7 +336,7 @@ fn order_construction(
= scenes: Res<BuildingScenes>,
=) {
= let count = parcels.iter().count();
- info!("There are {count} parcels now.");
+ debug!("There are {count} parcels now.");
=
= let population = people.iter().count();
= let capacity = buildings.iter().count();
@@ -370,7 +370,7 @@ fn order_construction(
= capacity: HousingCapacity(2),
= residents: None,
= };
- info!("Building a new house {description:#?}");
+ debug!("Building a new house {description:#?}");
= build_events.send(ConstructionOrder(description));
= }
=}
@@ -393,7 +393,7 @@ fn construct_building(
= scenes: Res<BuildingScenes>,
= mut register: ResMut<BuildingsRegister>,
=) {
- info!("Constructing {description:#?}",);
+ debug!("Constructing {description:#?}",);
= let id = description.id.clone();
= let HousingCapacity(capacity) = description.capacity.clone();
= let residents = description.residents.clone().unwrap_or_default();
@@ -409,7 +409,7 @@ fn construct_building(
=
= let entity = entity.id();
=
- info!("Registering the new house {id:#?} -> {entity:#?}");
+ debug!("Registering the new house {id:#?} -> {entity:#?}");
=
= register.0.insert(id, entity);
=}index 161c7c3..2b48c72 100644
--- a/src/configuration.rs
+++ b/src/configuration.rs
@@ -8,7 +8,7 @@ impl Plugin for ConfigurationPlugin {
= fn build(&self, app: &mut App) {
= app.register_type::<Configuration>()
= .add_plugins(BevyArgsPlugin::<Configuration>::default())
- .add_systems(PreStartup, print_configuration);
+ .add_systems(Startup, print_configuration);
= }
=}
=
@@ -16,13 +16,24 @@ impl Plugin for ConfigurationPlugin {
=#[reflect(Resource)]
=#[command(about = "Otterhide town simulator", version)]
=pub struct Configuration {
+ /// Simulation .ron file path relative to assets/ directory
= #[arg(long)]
= #[serde(default)]
= pub load: Option<String>,
=
+ /// Which inspection panel to display if any
= #[arg(long)]
= #[serde(default)]
= pub inspect: Option<InspectorPanel>,
+
+ /// Number of years to simulate
+ #[arg(long, default_value = "150")]
+ #[serde(default = "default_duration")]
+ pub duration: u16,
+}
+
+fn default_duration() -> u16 {
+ 150
=}
=
=fn print_configuration(configuration: Res<Configuration>) {index b541b35..1813c19 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -82,7 +82,7 @@ fn advance_simulation_date(
= framecounter.0 = u8::rem_euclid(framecounter.0 + 1, simulation.frames_per_day);
= if framecounter.0 == 0 {
= date.0.advance_to_next().advance(simulation.frame_offset);
- info!("New month: {month}", month = date.0);
+ debug!("New month: {month}", month = date.0);
= // IDEA: Maybe we can get rid of it and let all systems look into the frame counter?
= new_month.send(NewMonth);
= } else {index 5e37ff1..92bc87e 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -112,7 +112,7 @@ fn setup_districts(
=
= // TODO: Do it for every scene
= for (name, scene_handle) in districts_gltf.named_scenes.iter() {
- info!("Processing district {name}");
+ debug!("Processing district {name}");
= let Ok(variant) = name.parse::<DistrictVariant>() else {
= panic!("The districts asset contains a scene that can't be parsed into a DistrictVariant: {name}");
= };
@@ -470,7 +470,7 @@ fn plan_new_districts(
= if population < capacity {
= return;
= }
- info!("Not enough living space ({population} / {capacity}). Planning a new district.");
+ debug!("Not enough living space ({population} / {capacity}). Planning a new district.");
=
= // TODO: Use some smarter heuristics to choose the district
= let variant = scenes.0.keys().choose(&mut thread_rng()).unwrap();
@@ -526,7 +526,7 @@ fn plan_new_districts(
= return false;
= }
= if i32::from(&candidate.north()) * i32::from(&candidate.south()) < 0 {
- info!("District would cross the longitudinal highway");
+ debug!("District would cross the longitudinal highway");
= return false;
= }
=
@@ -544,11 +544,11 @@ fn plan_new_districts(
= });
=
= let Some(selected) = candidates.choose(&mut thread_rng()) else {
- info!("Can't find a suitable spot for a new district!");
+ debug!("Can't find a suitable spot for a new district!");
= return;
= };
=
- info!("New district established: {selected:#?}");
+ debug!("New district established: {selected:#?}");
= new_districts.send(NewDistrictEstablished(selected));
=}
=
@@ -629,7 +629,7 @@ fn construct_district(
= roads_systems: Res<RoadsSystems>,
= mut commands: Commands,
=) {
- info!("Spawning a district: {description:#?}");
+ debug!("Spawning a district: {description:#?}");
=
= commands.run_system_with_input(
= roads_systems.implement_road_plan,index a4c2b9e..f4aa432 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -307,7 +307,7 @@ fn apply_slider_value(
= mut commands: Commands,
=) {
= let index = slider_value.0 as usize;
- info!(
+ debug!(
= "Loading snapshot {index} out of {count}",
= count = snapshots.collection.len()
= );index f2d1f06..6c8b2b3 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -163,22 +163,22 @@ fn register_historical_events(
= let date = date.0;
= for event in new_districts.read() {
= let event = HistoricalEvent::NewDistrictEstablished(event.to_owned());
- info!("Registering a historical event on {date:?}: {event:#?}");
+ debug!("Registering a historical event on {date:?}: {event:#?}");
= history.events.push(EventLogEntry { event, date });
= }
= for event in construction_orders.read() {
= let event = HistoricalEvent::ConstructionOrder(event.to_owned());
- info!("Registering a historical event on {date:?}: {event:#?}");
+ debug!("Registering a historical event on {date:?}: {event:#?}");
= history.events.push(EventLogEntry { event, date });
= }
= for event in immigrated.read() {
= let event = HistoricalEvent::Immigrated(event.to_owned());
- info!("Registering a historical event on {date:?}: {event:#?}");
+ debug!("Registering a historical event on {date:?}: {event:#?}");
= history.events.push(EventLogEntry { event, date });
= }
= for event in moved_in.read() {
= let event = HistoricalEvent::MovedIn(event.to_owned());
- info!("Registering a historical event on {date:?}: {event:#?}");
+ debug!("Registering a historical event on {date:?}: {event:#?}");
= history.events.push(EventLogEntry { event, date });
= }
=}
@@ -207,7 +207,7 @@ pub fn take_snapshot(world: &mut World) {
= let buildings = BuildingsSnapshot::capture(world);
= let population = PopulationSnapshot::capture(world);
=
- info!("Registering a new snapshot on {date:#?}");
+ debug!("Registering a new snapshot on {date:#?}");
=
= let snapshot = MainSnapshot {
= date,
@@ -223,7 +223,7 @@ pub fn take_snapshot(world: &mut World) {
=}
=
=fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
- info!("Restoring snapshot {}", snapshot.date.0);
+ debug!("Restoring snapshot {}", snapshot.date.0);
= // Make sure no pending events from the future are going to reach systems
= // after the rollback
= world.run_system_once(clear_pending_historical_events);
@@ -288,7 +288,7 @@ fn replay_historical_events(
=) {
= future.events.retain(|entry| {
= if entry.date <= date.0 {
- info!("Replaying a historical event {entry:#?} (now is {date:#?})");
+ debug!("Replaying a historical event {entry:#?} (now is {date:#?})");
=
= match entry.event.clone() {
= HistoricalEvent::ConstructionOrder(order) => {index dcfc919..6db9c47 100644
--- a/src/major_state.rs
+++ b/src/major_state.rs
@@ -79,10 +79,12 @@ fn exit_loading_state(
= let next_state = MajorState::GettingReady;
=
= if pending_count == 0 {
- info!("All assets loaded. Switching from {current_state:?} to {next_state:?}");
+ info!(
+ "All {total_count} assets loaded. Switching from {current_state:?} to {next_state:?}"
+ );
= state.set(next_state);
= } else {
- info!("Waiting for {pending_count} of {total_count} assets to preload",);
+ debug!("Waiting for {pending_count} of {total_count} assets to preload",);
= }
=}
=
@@ -113,7 +115,7 @@ fn exit_getting_ready_state(world: &mut World) {
= info!("All {total_count} subsystem(s) ready. Switching from {current_state:?} to {next_state:?}");
= world.resource_scope(|_, mut state: Mut<NextState<MajorState>>| state.set(next_state));
= } else {
- info!("Waiting for {pending_count} of {total_count} subsystem(s) to get ready",);
+ debug!("Waiting for {pending_count} of {total_count} subsystem(s) to get ready",);
= }
=}
=index 057615a..27d32f8 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -123,7 +123,7 @@ pub struct Immigrated(pub PersonDescription);
=
=fn plan_immigration(mut immigrated: EventWriter<Immigrated>) {
= let number = 20;
- info!("{number} people immigrated to Otterhide.");
+ debug!("{number} people immigrated to Otterhide.");
=
= for _index in 1..=number {
= let person = random::<PersonDescription>();
@@ -137,7 +137,7 @@ fn implement_immigration(
= systems: Res<PopulationSystems>,
=) {
= for Immigrated(person) in immigrated.read().cloned() {
- info!("New immigrant {person:#?} ");
+ debug!("New immigrant {person:#?} ");
= commands.run_system_with_input(systems.setup_new_person, person);
= }
=}
@@ -172,7 +172,7 @@ fn move_into_houses(
= mut commands: Commands,
=) {
= for event in moved_in.read() {
- info!("Moving into a house {event:#?}");
+ debug!("Moving into a house {event:#?}");
=
= let MovedIn { who, where_to } = event;
= let Some(person) = persons_register.0.get(who) else {
@@ -311,7 +311,7 @@ impl Snapshot for PopulationSnapshot {
=
= for person in self.persons.iter() {
= world.resource_scope(|world, systems: Mut<PopulationSystems>| {
- info!("Restoring {person:#?}");
+ debug!("Restoring {person:#?}");
= world
= .run_system_with_input(systems.setup_new_person, person.clone())
= .unwrap()Speed up SQLite export of large simulations
On by
For our 150 year simulation it was too slow to be useful. More than 10 minutes to create a .db file.
Following some advise from https://stackoverflow.com/q/1711631/1151982 I got it to perform much better, down to a bout 30 seconds. What I did is:
- Use pool instead of single connection (no observable difference)
- Use in-memory journal (big improvement)
- Turn off synchronous writes (another big improvement)
The last two settings can be dangerous in case of a crash. They might leave the database in a corrupted state. However in our use case it doesn't matter much. The database is only an intermediate artifact on the way to get a SQL file, and if it get's corrupted it can always be recreated from the .ron file.
After the export we have: 125 districts, 11k buildings, 36k persons.
index 192351f..df55626 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -4,7 +4,7 @@ use otterhide::buildings::ConstructionOrder;
=use otterhide::districts::NewDistrictEstablished;
=use otterhide::history::{EventLogEntry, HistoricalEvent, History};
=use otterhide::population::{Immigrated, MovedIn};
-use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
+use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous};
=use std::path::PathBuf;
=
=#[derive(Parser)]
@@ -38,22 +38,26 @@ async fn main() -> anyhow::Result<()> {
=
= let connect_options = SqliteConnectOptions::new()
= .create_if_missing(true)
+ .journal_mode(SqliteJournalMode::Memory)
+ .synchronous(SqliteSynchronous::Off)
= .filename(&db_filename);
= let pool = SqlitePool::connect_with(connect_options)
= .await
= .context("Connecting to the database")?;
=
- let mut connection = pool
+ let connection = pool
= .acquire()
= .await
= .context("Acquiring connection to the database")?;
=
= let schema_sql = include_str!("../sqlite-export-schema.sql");
= sqlx::query(schema_sql)
- .execute(&mut *connection)
+ .execute(&pool)
= .await
= .context("Setting up the schema")?;
=
+ let transaction = pool.begin().await?;
+
= for EventLogEntry { date, event } in simulation.events.iter() {
= match event {
= HistoricalEvent::ConstructionOrder(ConstructionOrder(building)) => {
@@ -73,14 +77,14 @@ async fn main() -> anyhow::Result<()> {
= .bind(date.month() as u8)
= .bind(building.transform.translation.x)
= .bind(building.transform.translation.z)
- .execute(&mut *connection)
+ .execute(&pool)
= .await
= .context("Inserting a building")?;
= }
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {
= sqlx::query("Insert into district (name) values (?)")
= .bind(district.name.to_string())
- .execute(&mut *connection)
+ .execute(&pool)
= .await
= .context("Inserting a district")?;
= }
@@ -95,7 +99,7 @@ async fn main() -> anyhow::Result<()> {
= .bind(person.id.to_string())
= .bind(person.name.to_string())
= .bind(person.sex.to_string())
- .execute(&mut *connection)
+ .execute(&pool)
= .await
= .context("Inserting a person")?;
=
@@ -109,7 +113,7 @@ async fn main() -> anyhow::Result<()> {
= .bind(person.id.to_string())
= .bind(date.year())
= .bind(date.month() as u8)
- .execute(&mut *connection)
+ .execute(&pool)
= .await
= .context("Inserting immigration data")?;
= }
@@ -126,12 +130,13 @@ async fn main() -> anyhow::Result<()> {
= .bind(where_to.to_string())
= .bind(date.year())
= .bind(date.month() as u8)
- .execute(&mut *connection)
+ .execute(&pool)
= .await
= .context("Inserting immigration data")?;
= }
= }
= }
=
+ transaction.commit().await?;
= connection.close().await.context("Closing the connection")
=}Disable World Inspector Egui in release builds
On by
It is a significant drag on performance.
It will still be there in debug builds.
index 89bf061..8e8ecd9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,8 @@
=use bevy::prelude::*;
=use bevy::utils::Uuid;
+#[cfg(not(debug_assertions))]
+use bevy_egui::EguiPlugin;
+#[cfg(debug_assertions)]
=use bevy_inspector_egui::quick::WorldInspectorPlugin;
=use otterhide::buildings::BuildingsPlugin;
=use otterhide::camera::CameraPlugin;
@@ -43,7 +46,7 @@ fn main() {
= .add_plugins(DatePlugin)
= .add_plugins(SunPlugin)
= .add_plugins(HistoryPlugin)
- .add_plugins(WorldInspectorPlugin::new())
+ .add_plugins(DebugPlugin)
= .add_plugins(HudPlugin)
= .add_plugins(PopulationPlugin)
= .add_systems(Startup, greet)
@@ -53,3 +56,14 @@ fn main() {
=fn greet() {
= info!("Let's build the city on rock and roll!")
=}
+
+struct DebugPlugin;
+
+impl Plugin for DebugPlugin {
+ fn build(&self, app: &mut App) {
+ #[cfg(debug_assertions)]
+ app.add_plugins(WorldInspectorPlugin::new());
+ #[cfg(not(debug_assertions))]
+ app.add_plugins(EguiPlugin);
+ }
+}Disable camera while getting ready
On by
It speeds up the recreation of snapshots process, at the expense of displaying a dull, dark screen. Maybe we can put something entertaining there, like newsflashes from the city history.
index 035d7c6..6c3be92 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -3,12 +3,16 @@ use bevy_panorbit_camera::PanOrbitCamera;
=use bevy_panorbit_camera::PanOrbitCameraPlugin;
=use std::f32::consts::TAU;
=
+use crate::major_state::MajorState;
+
=pub struct CameraPlugin;
=
=impl Plugin for CameraPlugin {
= fn build(&self, app: &mut App) {
= app.add_plugins(PanOrbitCameraPlugin)
= .add_systems(Startup, setup_camera)
+ .add_systems(OnEnter(MajorState::GettingReady), disable_camera)
+ .add_systems(OnExit(MajorState::GettingReady), enable_camera)
= .add_systems(Update, limit_camera);
= }
=}
@@ -28,6 +32,16 @@ fn setup_camera(mut commands: Commands) {
= });
=}
=
+fn disable_camera(mut cameras: Query<&mut Camera>) {
+ let mut camera = cameras.single_mut();
+ camera.is_active = false;
+}
+
+fn enable_camera(mut cameras: Query<&mut Camera>) {
+ let mut camera = cameras.single_mut();
+ camera.is_active = false;
+}
+
=// TODO: Find a better way to lock camera focus on the ground. Current solution
=// is a kludge that doesn't work very well, esp. when the camera is low.
=// Probably disable default panning system and instead control focus by mappingRemove the obsolete serve goal form our Makefile
On by
There is a better web/serve goal.
index f740736..9534543 100644
--- a/Makefile
+++ b/Makefile
@@ -36,11 +36,6 @@ clean:
= --exclude='!.envrc.private'
=.PHONY: clean
=
-serve: ## Serve the web version of the program
-serve: web
- miniserve --interfaces=127.0.0.1 --index=index.html web
-.PHONY: serve
-
=check: ## Check the code
=check:
= cargo testFix: Camera always disabled
On by
How did this happen?
index 6c3be92..1c3ed2b 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -39,7 +39,7 @@ fn disable_camera(mut cameras: Query<&mut Camera>) {
=
=fn enable_camera(mut cameras: Query<&mut Camera>) {
= let mut camera = cameras.single_mut();
- camera.is_active = false;
+ camera.is_active = true;
=}
=
=// TODO: Find a better way to lock camera focus on the ground. Current solutionAssign birth dates to people
On by
In preparation for birth and death system, which (while good in its own right) should also ease on memory consumption. Currently people live forever and this make snapshots huge.
index 27d32f8..44a4a3d 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,6 +1,7 @@
=use crate::buildings::{BuildingId, HasSpareCapacity};
-use crate::date;
=use crate::date::SimulationFrameCounter;
+use crate::date::{self, Date};
+use crate::day_month::DayMonth;
=use crate::history::Snapshot;
=use crate::major_state::MajorState;
=use crate::names::{FEMALE_NAMES, LAST_NAMES, MALE_NAMES};
@@ -65,9 +66,26 @@ pub struct PersonDescription {
= pub sex: Sex,
= pub savings: Savings,
= pub residence: Option<BuildingId>,
+ pub birthdate: BirthDate,
=}
=
-impl Distribution<PersonDescription> for Standard {
+/// A statistical distribution that holds the current date
+///
+/// so that randomly generated persons are not too old and not born in the
+/// future.
+struct PersonsDistribution {
+ current_date: DayMonth,
+}
+
+impl From<Date> for PersonsDistribution {
+ fn from(date: Date) -> Self {
+ Self {
+ current_date: date.0,
+ }
+ }
+}
+
+impl Distribution<PersonDescription> for PersonsDistribution {
= fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> PersonDescription {
= let id = Uuid::from_bytes(rng.gen()).into();
= let sex = rng.gen::<Sex>();
@@ -82,12 +100,15 @@ impl Distribution<PersonDescription> for Standard {
= third: None,
= }
= };
+ let age: f32 = rng.gen_range(1.0..120.0);
+ let birthdate = self.current_date.clone().advance(-age).to_owned().into();
= let savings = rng.gen_range(100.0..10000.0).into();
= PersonDescription {
= id,
= name,
= savings,
= sex,
+ birthdate,
= residence: None,
= }
= }
@@ -121,12 +142,13 @@ pub fn setup_new_person(
=#[derive(Event, Debug, Clone, From, Serialize, Deserialize)]
=pub struct Immigrated(pub PersonDescription);
=
-fn plan_immigration(mut immigrated: EventWriter<Immigrated>) {
+fn plan_immigration(mut immigrated: EventWriter<Immigrated>, date: Res<Date>) {
= let number = 20;
= debug!("{number} people immigrated to Otterhide.");
=
+ let distribution = &PersonsDistribution::from(date.clone());
= for _index in 1..=number {
- let person = random::<PersonDescription>();
+ let person: PersonDescription = distribution.sample(&mut thread_rng());
= immigrated.send(Immigrated::from(person));
= }
=}
@@ -188,6 +210,7 @@ pub struct PersonBundle {
= pub name: PersonName,
= pub sex: Sex,
= pub savings: Savings,
+ pub birth_date: BirthDate,
=
= // Derived from the above
= pub display_name: Name,
@@ -195,12 +218,19 @@ pub struct PersonBundle {
=}
=
=impl PersonBundle {
- pub fn new(id: PersonId, name: PersonName, sex: Sex, savings: Savings) -> Self {
+ pub fn new(
+ id: PersonId,
+ name: PersonName,
+ sex: Sex,
+ birth_date: BirthDate,
+ savings: Savings,
+ ) -> Self {
= let display_name = name.to_string().into();
= Self {
= id,
= name,
= sex,
+ birth_date,
= savings,
= marker: Person,
= display_name,
@@ -210,7 +240,13 @@ impl PersonBundle {
=
=impl From<PersonDescription> for PersonBundle {
= fn from(value: PersonDescription) -> Self {
- Self::new(value.id, value.name, value.sex, value.savings)
+ Self::new(
+ value.id,
+ value.name,
+ value.sex,
+ value.birthdate,
+ value.savings,
+ )
= }
=}
=
@@ -225,13 +261,16 @@ pub struct PersonId(pub Uuid);
=#[derive(Component, Reflect, Clone, Debug, From, Serialize, Deserialize)]
=pub struct Savings(pub f32);
=
+#[derive(Component, Clone, Debug, From, Serialize, Deserialize)]
+pub struct BirthDate(pub DayMonth);
+
=#[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)]
=pub enum Sex {
= Male,
= Female,
=}
=
-impl Distribution<Sex> for rand::distributions::Standard {
+impl Distribution<Sex> for Standard {
= fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Sex {
= if rng.gen_bool(0.5) {
= Sex::Male
@@ -282,18 +321,27 @@ pub struct PopulationSnapshot {
=
=impl Snapshot for PopulationSnapshot {
= fn capture(world: &mut World) -> Self {
- let mut query =
- world.query_filtered::<(&PersonId, &PersonName, &Sex, &Savings, Option<&Residence>), With<Person>>();
+ let mut query = world.query_filtered::<(
+ &PersonId,
+ &PersonName,
+ &Sex,
+ &BirthDate,
+ &Savings,
+ Option<&Residence>,
+ ), With<Person>>();
= // TODO: Can I satisfy the borrow checker without collecting the iterator?
= let persons = query
= .iter(world)
- .map(|(id, name, sex, savings, residence)| PersonDescription {
- id: id.clone(),
- name: name.clone(),
- sex: sex.clone(),
- savings: savings.clone(),
- residence: residence.map(|Residence(building_id)| building_id.clone()),
- })
+ .map(
+ |(id, name, sex, birthdate, savings, residence)| PersonDescription {
+ id: id.clone(),
+ name: name.clone(),
+ sex: sex.clone(),
+ birthdate: birthdate.clone(),
+ savings: savings.clone(),
+ residence: residence.map(|Residence(building_id)| building_id.clone()),
+ },
+ )
= .collect_vec();
=
= Self { persons }SQLite: include birth dates in exported data
On by
index df55626..dc42585 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -93,12 +93,16 @@ async fn main() -> anyhow::Result<()> {
= "Insert into person (
= id,
= name,
- sex
- ) values (?, ?, ?)",
+ sex,
+ birth_year,
+ birth_month
+ ) values (?, ?, ?, ?, ?)",
= )
= .bind(person.id.to_string())
= .bind(person.name.to_string())
= .bind(person.sex.to_string())
+ .bind(person.birthdate.0.year())
+ .bind(person.birthdate.0.month() as u8)
= .execute(&pool)
= .await
= .context("Inserting a person")?;index 1b5c221..02a3b6d 100644
--- a/src/sqlite-export-schema.sql
+++ b/src/sqlite-export-schema.sql
@@ -17,8 +17,9 @@ Drop table if exists person;
=Create table person (
= id uuid primary key not null,
= name text not null,
- sex text check (sex in ('♂', '♀')) not null
- -- TODO: date_of_birth
+ sex text check (sex in ('♂', '♀')) not null,
+ birth_year integer not null,
+ birth_month integer not null check (birth_month between 1 and 12)
= -- TODO: date_of_death
= -- TODO: mother
= -- TODO: fatherFix inconsistent variable naming (birth_date)
On by
Sometimes it would be spelled as one word, which is inconsistent and incorrect.
index dc42585..c1bcb72 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -101,8 +101,8 @@ async fn main() -> anyhow::Result<()> {
= .bind(person.id.to_string())
= .bind(person.name.to_string())
= .bind(person.sex.to_string())
- .bind(person.birthdate.0.year())
- .bind(person.birthdate.0.month() as u8)
+ .bind(person.birth_date.0.year())
+ .bind(person.birth_date.0.month() as u8)
= .execute(&pool)
= .await
= .context("Inserting a person")?;index 44a4a3d..05d47ef 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -66,7 +66,7 @@ pub struct PersonDescription {
= pub sex: Sex,
= pub savings: Savings,
= pub residence: Option<BuildingId>,
- pub birthdate: BirthDate,
+ pub birth_date: BirthDate,
=}
=
=/// A statistical distribution that holds the current date
@@ -101,14 +101,14 @@ impl Distribution<PersonDescription> for PersonsDistribution {
= }
= };
= let age: f32 = rng.gen_range(1.0..120.0);
- let birthdate = self.current_date.clone().advance(-age).to_owned().into();
+ let birth_date = self.current_date.clone().advance(-age).to_owned().into();
= let savings = rng.gen_range(100.0..10000.0).into();
= PersonDescription {
= id,
= name,
= savings,
= sex,
- birthdate,
+ birth_date,
= residence: None,
= }
= }
@@ -244,7 +244,7 @@ impl From<PersonDescription> for PersonBundle {
= value.id,
= value.name,
= value.sex,
- value.birthdate,
+ value.birth_date,
= value.savings,
= )
= }
@@ -333,11 +333,11 @@ impl Snapshot for PopulationSnapshot {
= let persons = query
= .iter(world)
= .map(
- |(id, name, sex, birthdate, savings, residence)| PersonDescription {
+ |(id, name, sex, birth_date, savings, residence)| PersonDescription {
= id: id.clone(),
= name: name.clone(),
= sex: sex.clone(),
- birthdate: birthdate.clone(),
+ birth_date: birth_date.clone(),
= savings: savings.clone(),
= residence: residence.map(|Residence(building_id)| building_id.clone()),
= },Remove pseudo-PostgreSQL export code
On by
It was a lot of dead code that never really worked. The focus is now on SQLite export, which is much easier to develop. Once we get the system to relatively stable state, we will implement export to other databases.
index c9c8b2e..31ceb8f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -11,7 +11,6 @@ pub mod history;
=pub mod major_state;
=pub mod names;
=pub mod parcels;
-pub mod pgsql_export;
=pub mod population;
=pub mod roads;
=pub mod ron_export;deleted file mode 100644
index fafb3f7..0000000
--- a/src/pgsql_export.rs
+++ /dev/null
@@ -1,107 +0,0 @@
-use crate::buildings::{BuildingDescription, ConstructionOrder};
-use crate::districts::NewDistrictEstablished;
-use crate::history::{HistoricalEvent, History};
-use crate::population::{Immigrated, MovedIn};
-use bevy::math::Vec3;
-use std::fmt::Display;
-
-// TODO: Extract the export module into a standalone CLI program
-//
-// It should take RON from export_ron as input and outputs SQL, like that:
-//
-// $ cat otterhide.ron | otterhide-to-pgsql > otterhide.sql
-
-pub fn export(history: &History) -> String {
- history.to_string()
-}
-
-impl Display for History {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- writeln!(f, "Create table districts (")?;
- writeln!(f, " id primary key int autoincrement,")?;
- writeln!(f, " date timestamp without time zone not null,")?;
- writeln!(f, " coordinates point not null")?;
- writeln!(f, ");")?;
- writeln!(f, "Create table buildings (")?;
- writeln!(f, " id primary key int autoincrement,")?;
- writeln!(f, " date timestamp without time zone not null,")?;
- writeln!(f, " coordinates point not null")?;
- writeln!(f, ");")?;
-
- #[allow(clippy::zero_prefixed_literal)]
- for event in self.events.iter() {
- let date = event.date;
- let year = date.year();
- let month = date.month() as i32;
- let day = 01;
- let hour = date.hour();
- let minute = date.minute();
- let second = 00;
-
- match event.event.clone() {
- HistoricalEvent::ConstructionOrder(ConstructionOrder(BuildingDescription {
- id,
- address,
- transform,
- variant,
- capacity,
- residents: _,
- })) => {
- let Vec3 { x, z, .. } = transform.translation;
- let capacity = capacity.0;
-
- writeln!(f, "Insert into buildings (")?;
- writeln!(f, " id,")?;
- writeln!(f, " date,")?;
- writeln!(f, " variant,")?;
- writeln!(f, " address,")?;
- writeln!(f, " coordinates")?;
- writeln!(f, " capacity")?;
- writeln!(f, ") values (")?;
- writeln!(f, " {id}")?;
- writeln!(
- f,
- " {year}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"
- )?;
- writeln!(f, " {variant}")?;
- writeln!(f, " {address}")?;
- writeln!(f, " ({x:.3}, {z:.3})")?;
- writeln!(f, " {capacity}")?;
- writeln!(f, ");")?;
- }
- HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {
- writeln!(f, "Insert into districts (")?;
- writeln!(f, " date,")?;
- writeln!(f, " coordinates")?;
- writeln!(f, ") values (")?;
- writeln!(
- f,
- " {year}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"
- )?;
- writeln!(f, " {coordinates}", coordinates = district.origin)?;
- writeln!(f, ");")?;
- }
- HistoricalEvent::Immigrated(Immigrated(person)) => {
- writeln!(f, "Insert into person (")?;
- writeln!(f, " date_of_arrival,")?;
- writeln!(f, " name")?;
- writeln!(f, ") values (")?;
- writeln!(
- f,
- " {year}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"
- )?;
- writeln!(f, " {name}", name = person.name)?;
- writeln!(f, ");")?;
- }
- HistoricalEvent::MovedIn(MovedIn { who, where_to }) => {
- writeln!(
- f,
- "Insert into residency (resident, building) values ({who}, {where_to});"
- )?;
- }
- };
- }
-
- writeln!(f, "-- This is the end")
- }
-}Fix a typo
On by
index 71db7d9..d0c0b0f 100644
--- a/src/sun.rs
+++ b/src/sun.rs
@@ -44,7 +44,7 @@ fn move_sun(
= date: Res<Date>,
= settings: Res<SimulationParameters>,
=) {
- // TODO: Take elevation into account, so that the gap is actually between the endge of land and the sun
+ // TODO: Take elevation into account, so that the gap is actually between the edge of land and the sun
= let orbit = settings.land_radius + settings.sun_gap;
=
= let daytime = date.0.time_of_day();Limit exploration frame to 1h
On by
I suspect that some crashes were caused by very high replay speeds (e.g. 128x) making the exploration frame longer than a simulation frame. If the simulation frame is skipped over, it may lead to invalid state. I find it really difficult to debug those kind of things. They logic is asynchronous, multi-threaded and non-deterministic 😱 A lot of nasty bugs are caused when systems that implicitly depend on one another are running in unexpected order or even in parallel.
To help with debugging systems timing issues I added a log message on every simulation frame. With this new debug messages I can at least see if the failing systems ran on the same frame, or not.
index 1813c19..3f64e1b 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -79,14 +79,15 @@ fn advance_simulation_date(
= mut framecounter: ResMut<SimulationFrameCounter>,
= simulation: Res<SimulationParameters>,
=) {
- framecounter.0 = u8::rem_euclid(framecounter.0 + 1, simulation.frames_per_day);
+ framecounter.0 = u8::rem_euclid(framecounter.0 + 1, simulation.frames_per_day_month);
= if framecounter.0 == 0 {
= date.0.advance_to_next().advance(simulation.frame_offset);
- debug!("New month: {month}", month = date.0);
+ debug!("New simulation frame + new month: {:?}", date.0);
= // IDEA: Maybe we can get rid of it and let all systems look into the frame counter?
= new_month.send(NewMonth);
= } else {
- date.0.advance(1.0 / simulation.frames_per_day as f32);
+ date.0.advance(1.0 / simulation.frames_per_day_month as f32);
+ debug!("New simulation frame {:?}", date.0);
= }
=}
=
@@ -107,7 +108,7 @@ fn advance_exploration_date(
=
= let delta = time.delta_seconds();
= let duration = delta * scale;
- date.0.advance(duration);
+ date.0.advance(duration.min(HOUR));
=}
=
=/// Advances the date to the first event in the futureIntroduce death
On by
People are now have a random chance of dying that increases with age. Currently they live about 20 years. That is fine, as there is no reproduction yet, so only replacement is via immigration. But I think once reproduction is introduced, the lifespan should be reduced to 10 years or so, so we can have more generations in the data set.
Implementing this was tough. A naive way I took first was to send a Died event and have other systems take care of despawning the person, deregistering them from PersonsRegister and removing from Residents of a building. This last part proved problematic. If a person MovedIn and Died events for the same person would be sent on the same frame, then dependent on the order of execution, the system could attempt to remove them from building before they were added. This would lead to a panic.
To mitigate it, first a Dead marker is added to a person who died, and then on the next frame they are deregistered and despawned and removed from the building they resided in. It happens on the next frame, because remove_dead is scheduled PreUpdate, so before mark_dead.
There is a new one-shot system in buildings called remove_resident that is ran from population::remove_dead. This couples this two modules and I don't like it very much, but it makes things easier and seems to be working well, so I will live with it for now.
Generally at this moment I find systems coordination the hardest part of developing this project, especially when shared resources (like BuildingRegister and PersonsRegister) are involved. I wish to find a good pattern to work with it.
index c1bcb72..c2dc4d6 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -3,7 +3,7 @@ use clap::Parser;
=use otterhide::buildings::ConstructionOrder;
=use otterhide::districts::NewDistrictEstablished;
=use otterhide::history::{EventLogEntry, HistoricalEvent, History};
-use otterhide::population::{Immigrated, MovedIn};
+use otterhide::population::{Died, Immigrated, MovedIn};
=use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous};
=use std::path::PathBuf;
=
@@ -121,6 +121,20 @@ async fn main() -> anyhow::Result<()> {
= .await
= .context("Inserting immigration data")?;
= }
+ HistoricalEvent::Died(Died { person }) => {
+ sqlx::query(
+ "Update person set
+ death_year = ?,
+ death_month = ?
+ where id = ?",
+ )
+ .bind(date.year())
+ .bind(date.month() as u8)
+ .bind(person.to_string())
+ .execute(&pool)
+ .await
+ .context("Registering death")?;
+ }
= HistoricalEvent::MovedIn(MovedIn { where_to, who }) => {
= sqlx::query(
= "Insert into residency (index 8655f87..a4268ed 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -17,6 +17,7 @@ use rand::{thread_rng, Rng};
=use regex::Regex;
=use serde::{Deserialize, Serialize};
=use std::fmt::Display;
+use std::ops::Not;
=use std::str::FromStr;
=
=pub struct BuildingsPlugin;
@@ -74,15 +75,18 @@ pub struct BuildingsRegister(pub HashMap<BuildingId, Entity>);
=pub struct BuildingsSystems {
= pub construct_building: SystemId<BuildingDescription>,
= pub register_resident: SystemId<MovedIn>,
+ pub remove_resident: SystemId<(BuildingId, PersonId)>,
=}
=
=fn register_buildings_systems(world: &mut World) {
= let construct_building = world.register_system(construct_building);
= let register_resident = world.register_system(register_resident);
+ let remove_resident = world.register_system(remove_resident);
=
= world.insert_resource(BuildingsSystems {
= construct_building,
= register_resident,
+ remove_resident,
= });
=}
=
@@ -119,6 +123,39 @@ fn register_resident(
= }
=}
=
+fn remove_resident(
+ In((building, person)): In<(BuildingId, PersonId)>,
+ mut buildings: Query<(&HousingCapacity, Option<&mut Residents>), With<Building>>,
+ buildings_register: ResMut<BuildingsRegister>,
+ mut commands: Commands,
+) {
+ debug!("Removing resident {person:?} from building {building:?}");
+ let Some(building) = buildings_register.0.get(&building) else {
+ panic!("Building {building:#?} not in the registry {buildings_register:#?}");
+ };
+
+ let (capacity, residents) = buildings.get_mut(*building).unwrap();
+ match residents {
+ None => {
+ error!("There are no residents in building {building:?}. Can't remove {person:?}.");
+ }
+ Some(mut residents) => {
+ if residents.0.remove(&person).not() {
+ error!("The person {person:?} is not a resident at {building:?}. Residents are {residents:#?}");
+ }
+ if residents.0.len() <= capacity.0 as usize {
+ commands.entity(*building).remove::<IsOvercrowded>();
+ }
+ if residents.0.len() < capacity.0 as usize {
+ commands.entity(*building).insert(HasSpareCapacity);
+ }
+ if residents.0.is_empty() {
+ commands.entity(*building).remove::<Residents>();
+ }
+ }
+ }
+}
+
=fn setup_assets(
= assets: Res<AssetServer>,
= mut commands: Commands,index 461c87c..51c610c 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -1,6 +1,6 @@
-use std::fmt::Display;
-
+use derive_more::Into;
=use serde::{Deserialize, Serialize};
+use std::fmt::Display;
=
=pub const HOUR: f32 = 1.0 / 24.0;
=pub const MINUTE: f32 = HOUR / 60.0;
@@ -47,6 +47,12 @@ impl Month {
= }
=}
=
+impl From<Month> for f32 {
+ fn from(val: Month) -> Self {
+ (val as isize - 1) as f32
+ }
+}
+
=#[cfg(test)]
=mod month_tests {
= use super::*;
@@ -109,6 +115,7 @@ impl DayMonth {
= self.advance(time_of_day)
= }
=
+ // TODO: Use the Duration type here
= pub fn advance(&mut self, duration: f32) -> &mut Self {
= let month = self.month + duration;
=
@@ -171,6 +178,69 @@ impl From<&DayMonth> for f32 {
= }
=}
=
+#[derive(Debug, PartialEq, PartialOrd, Into)]
+pub struct Duration {
+ pub months: f32,
+}
+
+impl Duration {
+ pub fn new(from: &DayMonth, until: &DayMonth) -> Duration {
+ let years = until.year - from.year;
+ let months = until.month - from.month;
+ Self {
+ months: (years * 12) as f32 + months,
+ }
+ }
+
+ pub fn years(&self) -> i32 {
+ self.months as i32 / 12
+ }
+}
+
+#[cfg(test)]
+mod duration_tests {
+ use super::*;
+
+ #[test]
+ fn new() {
+ let from = DayMonth {
+ year: 1903,
+ month: Month::April.into(),
+ };
+ let until = DayMonth {
+ year: 1905,
+ month: Month::February.into(),
+ };
+ let expected = Duration { months: 22.0 };
+ let actual = Duration::new(&from, &until);
+ assert_eq!(expected, actual);
+
+ let from = DayMonth {
+ year: 1905,
+ month: Month::February.into(),
+ };
+ let until = DayMonth {
+ year: 1903,
+ month: Month::April.into(),
+ };
+ let expected = Duration { months: -22.0 };
+ let actual = Duration::new(&from, &until);
+ assert_eq!(expected, actual);
+
+ let from = DayMonth {
+ year: 1992,
+ month: Month::June.into(),
+ };
+ let until = DayMonth {
+ year: 1992,
+ month: Month::June.into(),
+ };
+ let expected = Duration { months: 0.0 };
+ let actual = Duration::new(&from, &until);
+ assert_eq!(expected, actual);
+ }
+}
+
=#[cfg(test)]
=mod daymonth_tests {
= use super::*;index f4aa432..762f53c 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -1,12 +1,16 @@
+use std::borrow::Borrow;
+
=use crate::buildings::Building;
=use crate::buildings::BuildingsRegister;
=use crate::configuration::Configuration;
=use crate::date::{Date, DateSystems};
+use crate::day_month::Duration;
=use crate::history::Future;
=use crate::history::History;
=use crate::history::HistorySystems;
=use crate::history::Snapshots;
=use crate::major_state::MajorState;
+use crate::population::BirthDate;
=use crate::population::Person;
=use crate::population::PersonId;
=use crate::population::PersonsRegister;
@@ -321,10 +325,22 @@ fn apply_slider_value(
=
=fn paint_population_panel(
= mut contexts: EguiContexts,
- persons: Query<(&PersonId, &Name, &Sex, &Savings, Option<&Residence>), With<Person>>,
+ persons: Query<
+ (
+ &PersonId,
+ &Name,
+ &Sex,
+ &BirthDate,
+ &Savings,
+ Option<&Residence>,
+ ),
+ With<Person>,
+ >,
= buildings: Query<&Name, With<Building>>,
= persons_register: Res<PersonsRegister>,
= buildings_register: Res<BuildingsRegister>,
+
+ date: Res<Date>,
=) {
= egui::CentralPanel::default()
= .frame(Frame {
@@ -349,19 +365,25 @@ fn paint_population_panel(
= egui::Grid::new("persons-grid").show(ui, |ui| {
= ui.label("Name");
= ui.label("Sex");
+ ui.label("Age");
= ui.label("Savings");
= ui.label("Address");
= ui.label("Id");
= ui.end_row();
=
- for (id, name, sex, Savings(savings), residence) in persons.iter() {
+ for (id, name, sex, BirthDate(birth_date), Savings(savings), residence) in
+ persons.iter()
+ {
= let address = residence
= .and_then(|Residence(id)| buildings_register.0.get(id))
= .map(|entity| buildings.get(*entity).unwrap().as_str())
= .unwrap_or("-");
=
+ let age = Duration::new(birth_date, date.0.borrow()).years();
+
= ui.label(name.to_string());
= ui.label(sex.to_string());
+ ui.label(age.to_string());
= ui.label(format!("{savings:.2} Œ"));
= ui.label(address);
= ui.label(id.0.to_string());index 6c8b2b3..81a8ca1 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -4,7 +4,7 @@ use crate::day_month::{DayMonth, Month};
=use crate::districts::DistrictsSnapshot;
=use crate::districts::NewDistrictEstablished;
=use crate::major_state::{MajorState, ReadyPredicates};
-use crate::population::{Immigrated, MovedIn, PopulationSnapshot};
+use crate::population::{Died, Immigrated, MovedIn, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
=use crate::simulation::SimulationAssetHandle;
=use crate::Simulation;
@@ -112,6 +112,7 @@ pub enum HistoricalEvent {
= ConstructionOrder(ConstructionOrder),
= NewDistrictEstablished(NewDistrictEstablished),
= Immigrated(Immigrated),
+ Died(Died),
= MovedIn(MovedIn),
=}
=
@@ -148,6 +149,9 @@ impl Display for HistoricalEvent {
= HistoricalEvent::MovedIn(MovedIn { who, where_to }) => {
= write!(f, "The person {who} moved into the building {where_to}")
= }
+ HistoricalEvent::Died(Died { person: id }) => {
+ write!(f, "The person {id} died")
+ }
= }
= }
=}
@@ -159,6 +163,7 @@ fn register_historical_events(
= mut new_districts: EventReader<NewDistrictEstablished>,
= mut immigrated: EventReader<Immigrated>,
= mut moved_in: EventReader<MovedIn>,
+ mut died: EventReader<Died>,
=) {
= let date = date.0;
= for event in new_districts.read() {
@@ -181,6 +186,11 @@ fn register_historical_events(
= debug!("Registering a historical event on {date:?}: {event:#?}");
= history.events.push(EventLogEntry { event, date });
= }
+ for event in died.read() {
+ let event = HistoricalEvent::Died(event.to_owned());
+ debug!("Registering a historical event on {date:?}: {event:#?}");
+ history.events.push(EventLogEntry { event, date });
+ }
=}
=
=// TODO: Use hankjordan/bevy_save with a custom pipeline
@@ -285,6 +295,7 @@ fn replay_historical_events(
= mut districts: EventWriter<NewDistrictEstablished>,
= mut immigrated: EventWriter<Immigrated>,
= mut moved_in: EventWriter<MovedIn>,
+ mut died: EventWriter<Died>,
=) {
= future.events.retain(|entry| {
= if entry.date <= date.0 {
@@ -303,6 +314,9 @@ fn replay_historical_events(
= HistoricalEvent::MovedIn(event) => {
= moved_in.send(event);
= }
+ HistoricalEvent::Died(event) => {
+ died.send(event);
+ }
= };
= false
= } else {index 31ceb8f..d61d7fd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -46,7 +46,7 @@ pub struct SimulationParameters {
= pub years: u16,
= /// Day-months per one second of real time during simulation
= #[inspector(min = 2, max = 255, display = NumberDisplay::Slider)]
- pub frames_per_day: u8,
+ pub frames_per_day_month: u8,
= /// What time the first frame of the day starts
= ///
= /// 0.0 - midnight. 0.5 - noon
@@ -65,7 +65,7 @@ impl Default for SimulationParameters {
= #[cfg(not(debug_assertions))]
= years: 150,
= frame_offset: 2.0 * HOUR,
- frames_per_day: 6,
+ frames_per_day_month: 6,
= }
= }
=}index 05d47ef..57c156b 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,10 +1,11 @@
-use crate::buildings::{BuildingId, HasSpareCapacity};
+use crate::buildings::{BuildingId, BuildingsSystems, HasSpareCapacity};
=use crate::date::SimulationFrameCounter;
=use crate::date::{self, Date};
-use crate::day_month::DayMonth;
+use crate::day_month::{DayMonth, Duration};
=use crate::history::Snapshot;
=use crate::major_state::MajorState;
=use crate::names::{FEMALE_NAMES, LAST_NAMES, MALE_NAMES};
+use crate::SimulationParameters;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy::utils::{HashMap, Uuid};
@@ -28,6 +29,7 @@ impl Plugin for PopulationPlugin {
= .register_type::<PersonName>()
= .add_event::<Immigrated>()
= .add_event::<MovedIn>()
+ .add_event::<Died>()
= .add_systems(Startup, register_population_systems)
= .add_systems(
= Update,
@@ -43,6 +45,19 @@ impl Plugin for PopulationPlugin {
= .run_if(on_event::<Immigrated>())
= .before(search_for_houses),
= )
+ .add_systems(PreUpdate, remove_dead)
+ .add_systems(
+ Update,
+ plan_mortality
+ .before(mark_dead)
+ .after(move_into_houses)
+ .run_if(
+ in_state(MajorState::Simulating)
+ .and_then(resource_changed::<SimulationFrameCounter>)
+ .and_then(date::past_hour(6.0)),
+ ),
+ )
+ .add_systems(Update, mark_dead.run_if(on_event::<Died>()))
= .add_systems(
= Update,
= search_for_houses.before(move_into_houses).run_if(
@@ -54,6 +69,72 @@ impl Plugin for PopulationPlugin {
= }
=}
=
+// TODO: Add residency, so building can be updated too
+#[derive(Event, Clone, Serialize, Deserialize, Debug)]
+pub struct Died {
+ pub person: PersonId,
+}
+
+#[derive(Component, Debug)]
+pub struct Dead;
+
+/// Select people to die
+pub fn plan_mortality(
+ persons: Query<(&PersonId, &BirthDate, Option<&Residence>)>,
+ date: Res<Date>,
+ simulation: Res<SimulationParameters>,
+ mut died: EventWriter<Died>,
+) {
+ for (id, birth_date, residence) in persons.iter() {
+ let age = Duration::new(&birth_date.0, &date.0).years();
+ // TODO: Lower once we have reproduction system
+ const EXTREMELY_OLD: u32 = 20 * 10;
+
+ let numerator = age as u32 + 1;
+ let denominator = EXTREMELY_OLD.max(numerator + 5) * simulation.frames_per_day_month as u32;
+ if thread_rng().gen_ratio(numerator, denominator) {
+ let residence = residence.map(|value| value.0.clone());
+ debug!("Making {id:?} (residing at {residence:?}) dead");
+ died.send(Died { person: id.clone() });
+ };
+ }
+}
+
+/// Make people who died as Dead
+///
+/// So they can be gracefully removed on the next frame
+pub fn mark_dead(
+ mut died: EventReader<Died>,
+ mut commands: Commands,
+ register: ResMut<PersonsRegister>,
+) {
+ for Died { person, .. } in died.read() {
+ debug!("Marking the person {person:?} as dead");
+ let entity = register.0.get(person).unwrap();
+ commands.entity(*entity).insert(Dead);
+ }
+}
+
+/// Deregister and despawn dead persons
+pub fn remove_dead(
+ dead: Query<(Entity, &PersonId, Option<&Residence>), With<Dead>>,
+ mut register: ResMut<PersonsRegister>,
+ building_systems: Res<BuildingsSystems>,
+ mut commands: Commands,
+) {
+ for (entity, person, residence) in dead.iter() {
+ debug!("Removing the dead person {person:?}");
+ if let Some(Residence(building)) = residence {
+ commands.run_system_with_input(
+ building_systems.remove_resident,
+ (building.clone(), person.clone()),
+ )
+ }
+ register.0.remove(person);
+ commands.entity(entity).despawn_recursive();
+ }
+}
+
=#[derive(Resource, Clone, Default, Debug, Reflect)]
=#[reflect(Resource)]
=pub struct PersonsRegister(pub HashMap<PersonId, Entity>);index 02a3b6d..a8f88f2 100644
--- a/src/sqlite-export-schema.sql
+++ b/src/sqlite-export-schema.sql
@@ -18,11 +18,20 @@ Create table person (
= id uuid primary key not null,
= name text not null,
= sex text check (sex in ('♂', '♀')) not null,
+
= birth_year integer not null,
- birth_month integer not null check (birth_month between 1 and 12)
+ birth_month integer not null check (birth_month between 1 and 12),
+
+ death_year integer,
+ death_month integer check (death_month between 1 and 12),
+
+ -- Neither part of the death date can be null, unless both are
+ check ((death_year is null) == (death_month is null))
+
= -- TODO: date_of_death
= -- TODO: mother
= -- TODO: father
+
=);
=
=Drop table if exists migration;Allow to pass extra arguments to make develop
On by
index 9534543..b8de0ea 100644
--- a/Makefile
+++ b/Makefile
@@ -26,8 +26,9 @@ install: build
=### DEVELOPMENT
=
=develop: ## Rebuild and run a development version of the program
+develop: agrs := ""
=develop:
- cargo run --features bevy/dynamic_linking --bin otterhide
+ cargo run --features bevy/dynamic_linking --bin otterhide -- $(args)
=.PHONY: develop
=
=clean: ## Remove all build artifactsAllow setting beginning and duration via arguments
On by
There are two new arguments for command line and URL:
-
duration: how many years should be simulated
It was accidentally introduced few commits earlier, but had no effect. Now it does.
-
beginning: what should the first year be
Implementing this lead to a cascade of changes. Some of them should probably be their own commits. Sorry for the large scope in this one.
The SimulationParameters are now defined in the simulation module, and are part of the larger Simulation structure. This struct is used to serialize and deserialise simulations. So the parameters are now stored together with history.
The beginning and duration are no longer part of a simulation parameters. This may seem a bit counter intuitive, but otherwise they would be redundant information. The beginning and end of a saved simulation are determined by first and last event. So the duration only needs to be specified when generating a new simulation.
When a previously saved simulation is loaded, the parameters are populated from it. It happens in the new system called simulation::get_ready. It also populates the Future resource with events to be processed (previously this happened in history::import_simulation). If the simulation was not loaded (i.e. a new one is going to be generated), this get_ready system will initialize resources with their default values. Having this new system is handy, as a reference point to other initialization systems that depend on SimulationParameters or Future resource. They can be scheduled after the simulation::get_ready and be sure the required resources are there.
The Duration type has been enhnced with few new helper methods and is used much more, including to advance date and represent person's age.
The top-level lib.rs module is empty now. It only declares other modules. Also main.rs is almost clean. It's responsibility is to put all the plugins together and start the program.
index 2b48c72..4617f6e 100644
--- a/src/configuration.rs
+++ b/src/configuration.rs
@@ -12,28 +12,43 @@ impl Plugin for ConfigurationPlugin {
= }
=}
=
-#[derive(Resource, Clone, Debug, Default, Serialize, Deserialize, Parser, Reflect)]
+const DEFAULT_BEGINNING: u16 = 1840;
+#[cfg(debug_assertions)]
+const DEFAULT_DURATION: u16 = 10;
+#[cfg(not(debug_assertions))]
+const DEFAULT_DURATION: u16 = 150;
+
+#[derive(Resource, Clone, Debug, Serialize, Deserialize, Parser, Reflect)]
=#[reflect(Resource)]
+#[serde(default)]
=#[command(about = "Otterhide town simulator", version)]
=pub struct Configuration {
= /// Simulation .ron file path relative to assets/ directory
= #[arg(long)]
- #[serde(default)]
= pub load: Option<String>,
=
= /// Which inspection panel to display if any
= #[arg(long)]
- #[serde(default)]
= pub inspect: Option<InspectorPanel>,
=
= /// Number of years to simulate
- #[arg(long, default_value = "150")]
- #[serde(default = "default_duration")]
+ #[arg(long, default_value_t = DEFAULT_DURATION)]
= pub duration: u16,
+
+ /// First year of the simulation
+ #[arg(long, default_value_t = DEFAULT_BEGINNING)]
+ pub beginning: u16,
=}
=
-fn default_duration() -> u16 {
- 150
+impl Default for Configuration {
+ fn default() -> Self {
+ Self {
+ beginning: DEFAULT_BEGINNING,
+ duration: DEFAULT_DURATION,
+ inspect: None,
+ load: None,
+ }
+ }
=}
=
=fn print_configuration(configuration: Res<Configuration>) {index 3f64e1b..0497e7e 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -1,7 +1,9 @@
-use crate::day_month::{DayMonth, HOUR, MINUTE};
-use crate::history::{Future, Snapshot};
+use crate::configuration::Configuration;
+use crate::day_month::{DayMonth, Duration};
+use crate::history::{EventLogEntry, Future, History, Snapshot};
=use crate::major_state::MajorState;
-use crate::SimulationParameters;
+use crate::simulation::{self, SimulationParameters};
+use anyhow::Context;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
@@ -12,16 +14,17 @@ pub struct DatePlugin;
=
=impl Plugin for DatePlugin {
= fn build(&self, app: &mut App) {
- let simulation = app.world.resource::<SimulationParameters>();
- let date = simulation.beginning();
-
- app.insert_resource(Date(date))
- .register_type::<Timescale>()
+ app.register_type::<Timescale>()
= .init_resource::<Timescale>()
= .register_type::<SimulationFrameCounter>()
= .init_resource::<SimulationFrameCounter>()
= .add_event::<NewMonth>()
- .add_systems(Startup, setup_date_display)
+ .add_systems(
+ OnEnter(MajorState::GettingReady),
+ (initialize_date, setup_date_display)
+ .chain()
+ .after(simulation::get_ready),
+ )
= .add_systems(Startup, register_date_systems)
= .add_systems(
= PreUpdate,
@@ -31,25 +34,33 @@ impl Plugin for DatePlugin {
= PreUpdate,
= advance_exploration_date.run_if(in_state(MajorState::Exploring)),
= )
- .add_systems(PreUpdate, update_date_display);
+ .add_systems(
+ PreUpdate,
+ update_date_display.run_if(resource_exists::<Date>),
+ );
= }
=}
=
-#[derive(Resource, Debug, Clone, Serialize, Deserialize)]
-pub struct Date(pub DayMonth);
-
-impl Date {
- pub fn fraction_of_simulation(&self, simulation: &SimulationParameters) -> f32 {
- let months_to_simulate = (simulation.years * 12) as f32;
- self.months_simulated(simulation) / months_to_simulate
- }
- pub fn months_simulated(&self, simulation: &SimulationParameters) -> f32 {
- let first_month = (simulation.first_year * 12) as f32;
- let current_month = f32::from(&self.0);
- current_month - first_month
+fn initialize_date(world: &mut World) {
+ let future = world.resource::<Future>();
+ if let Some(EventLogEntry { date, .. }) = future.events.first() {
+ info!("Initializing date to {date} based of first unprocessed historical event");
+ world.insert_resource(Date(*date))
+ } else {
+ let simulation = world.resource::<SimulationParameters>();
+ let runtime = world.resource::<Configuration>();
+ let date = simulation.beginning(runtime);
+ info!("Initializing date to {date}");
+ world.insert_resource(Date(date));
= }
=}
=
+// fn initialize_getting_ready_date(world: &mut World) {
+// }
+
+#[derive(Resource, Debug, Clone, Serialize, Deserialize)]
+pub struct Date(pub DayMonth);
+
=/// Count the frames simulated so far
=#[derive(Resource, Default, Debug, Reflect)]
=#[reflect(Resource)]
@@ -81,12 +92,13 @@ fn advance_simulation_date(
=) {
= framecounter.0 = u8::rem_euclid(framecounter.0 + 1, simulation.frames_per_day_month);
= if framecounter.0 == 0 {
- date.0.advance_to_next().advance(simulation.frame_offset);
+ date.0.advance_to_next().advance(&simulation.frame_offset);
= debug!("New simulation frame + new month: {:?}", date.0);
= // IDEA: Maybe we can get rid of it and let all systems look into the frame counter?
= new_month.send(NewMonth);
= } else {
- date.0.advance(1.0 / simulation.frames_per_day_month as f32);
+ let duration = Duration::from_months(1.0 / simulation.frames_per_day_month as f32);
+ date.0.advance(&duration);
= debug!("New simulation frame {:?}", date.0);
= }
=}
@@ -96,19 +108,39 @@ fn advance_exploration_date(
= mut date: ResMut<Date>,
= timescale: Res<Timescale>,
= simulation: Res<SimulationParameters>,
+ history: Res<History>,
+ future: Res<Future>,
=) {
- // Do not go much over the duration, but gently slow down after
- const ENDTIME: f32 = 9.0 * HOUR + 32.0 * MINUTE;
-
- let scale = if date.0 > simulation.end() {
- timescale.scale * (ENDTIME - date.0.time_of_day()).max(0.0)
+ let slow_down_factor: f32 = if future.events.is_empty() {
+ // Do not go much over the duration, but gently slow down after
+ let end = history
+ .events
+ .last()
+ .context("Getting the last event from history to put a break on time advancement")
+ .unwrap()
+ .date;
+
+ // How much time passed since the end of the simulation
+ let overtime_duration = Duration::new(&end, &date.0);
+
+ // How long does it take to slow down time to a complete halt after the
+ // simulation is over
+ let slowdown_duration = Duration::from_hours(7.55);
+
+ (overtime_duration / slowdown_duration).min(1.0)
= } else {
- timescale.scale
+ 0.0
= };
=
+ let scale = timescale.scale * (1.0 - slow_down_factor);
+
+ // TODO: Figure out how to implement Ord for Duration and have .min and .max work on it
= let delta = time.delta_seconds();
- let duration = delta * scale;
- date.0.advance(duration.min(HOUR));
+ let raw_duration = delta * scale;
+ let raw_duration_limit = 1.0 / simulation.frames_per_day_month as f32;
+ let duration = Duration::from_months(raw_duration.min(raw_duration_limit));
+
+ date.0.advance(&duration);
=}
=
=/// Advances the date to the first event in the future
@@ -194,8 +226,14 @@ pub fn past_hour(hour: f32) -> impl FnMut(Option<Res<Date>>, Local<Option<DayMon
= };
=
= // On first run, pretend that it happened on previous daymonth
- let previous = last_ran.unwrap_or(*date.0.clone().set_hour(hour).advance(-1.0));
- let next = *previous.clone().advance(1.0);
+ let previous = last_ran.unwrap_or(
+ *date
+ .0
+ .clone()
+ .set_hour(hour)
+ .advance(&Duration::from_months(-1.0)),
+ );
+ let next = *previous.clone().advance(&Duration::from_months(1.0));
= let now = date.0;
=
= if now >= next {index 51c610c..ddbcbe1 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -1,9 +1,9 @@
-use derive_more::Into;
=use serde::{Deserialize, Serialize};
=use std::fmt::Display;
+use std::ops::{Div, Neg};
=
-pub const HOUR: f32 = 1.0 / 24.0;
-pub const MINUTE: f32 = HOUR / 60.0;
+const HOUR: f32 = 1.0 / 24.0;
+const MINUTE: f32 = HOUR / 60.0;
=
=#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
=pub enum Month {
@@ -112,12 +112,13 @@ impl DayMonth {
= /// i.e. decrement or increment the month and year.
= pub fn set_time_of_day(&mut self, time_of_day: f32) -> &mut Self {
= self.month = self.month.floor();
- self.advance(time_of_day)
+ self.advance(&Duration {
+ months: time_of_day,
+ })
= }
=
- // TODO: Use the Duration type here
- pub fn advance(&mut self, duration: f32) -> &mut Self {
- let month = self.month + duration;
+ pub fn advance(&mut self, duration: &Duration) -> &mut Self {
+ let month = self.month + duration.months;
=
= self.month = month.rem_euclid(12.0);
= self.year += month.div_euclid(12.0).floor() as i32;
@@ -126,7 +127,7 @@ impl DayMonth {
=
= /// Advance to the beginning of the next month
= pub fn advance_to_next(&mut self) -> &mut Self {
- self.advance(1.0);
+ self.advance(&Duration::from_months(1.0));
= self.month = self.month.floor();
= self
= }
@@ -178,7 +179,7 @@ impl From<&DayMonth> for f32 {
= }
=}
=
-#[derive(Debug, PartialEq, PartialOrd, Into)]
+#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
=pub struct Duration {
= pub months: f32,
=}
@@ -192,11 +193,51 @@ impl Duration {
= }
= }
=
+ pub fn from_years(years: f32) -> Self {
+ Self {
+ months: years * 12.0,
+ }
+ }
+
+ pub fn from_months(months: f32) -> Self {
+ Self { months }
+ }
+
+ pub fn from_hours(hours: f32) -> Self {
+ Self {
+ months: hours * HOUR,
+ }
+ }
+
+ pub fn from_minutes(minutes: f32) -> Self {
+ Self {
+ months: minutes * MINUTE,
+ }
+ }
+
= pub fn years(&self) -> i32 {
= self.months as i32 / 12
= }
=}
=
+impl Neg for Duration {
+ type Output = Self;
+
+ fn neg(self) -> Self::Output {
+ Self {
+ months: self.months.neg(),
+ }
+ }
+}
+
+impl Div for Duration {
+ type Output = f32;
+
+ fn div(self, rhs: Self) -> Self::Output {
+ self.months / rhs.months
+ }
+}
+
=#[cfg(test)]
=mod duration_tests {
= use super::*;
@@ -239,6 +280,19 @@ mod duration_tests {
= let actual = Duration::new(&from, &until);
= assert_eq!(expected, actual);
= }
+
+ #[test]
+ fn compare() {
+ let a = Duration::from_hours(1.0);
+ let b = Duration::from_hours(2.0);
+
+ assert!(a < b);
+ assert!(b > a);
+
+ // TODO: Figure out how to implement Ord for Duration and have .min and .max work
+ // assert_eq!(a.clone().min(b.clone()), a);
+ // assert_eq!(a.clone().max(b.clone()), b);
+ }
=}
=
=#[cfg(test)]
@@ -260,22 +314,22 @@ mod daymonth_tests {
= let mut daymonth = DayMonth::new(1984);
= assert_eq!(format!("{daymonth}"), "January 1984 00:00");
=
- daymonth.advance(1.0);
+ daymonth.advance(&Duration::from_months(1.0));
= assert_eq!(format!("{daymonth}"), "February 1984 00:00");
=
- daymonth.advance(45. * MINUTE);
+ daymonth.advance(&Duration::from_minutes(45.));
= assert_eq!(format!("{daymonth}"), "February 1984 00:45");
=
- daymonth.advance(23. * HOUR);
+ daymonth.advance(&Duration::from_hours(23.));
= assert_eq!(format!("{daymonth}"), "February 1984 23:45");
=
- daymonth.advance(30. * MINUTE);
+ daymonth.advance(&Duration::from_minutes(30.));
= assert_eq!(format!("{daymonth}"), "March 1984 00:15");
=
- daymonth.advance(13.5 * HOUR);
+ daymonth.advance(&Duration::from_hours(13.5));
= assert_eq!(format!("{daymonth}"), "March 1984 13:45");
=
- daymonth.advance(12. * HOUR);
+ daymonth.advance(&Duration::from_hours(12.));
= assert_eq!(format!("{daymonth}"), "April 1984 01:45");
= }
=
@@ -285,8 +339,8 @@ mod daymonth_tests {
=
= assert!(DayMonth::new(2000) < DayMonth::new(2001));
= assert!(DayMonth::new(2001) > DayMonth::new(2000));
- assert!(DayMonth::new(2000) < *DayMonth::new(2000).advance(1.0));
- assert!(DayMonth::new(2001) < *DayMonth::new(2000).advance(12.5));
- assert!(DayMonth::new(2000) > *DayMonth::new(2000).advance(-0.1));
+ assert!(DayMonth::new(2000) < *DayMonth::new(2000).advance(&Duration::from_months(1.0)));
+ assert!(DayMonth::new(2001) < *DayMonth::new(2000).advance(&Duration::from_months(12.5)));
+ assert!(DayMonth::new(2000) > *DayMonth::new(2000).advance(&Duration::from_months(-0.1)));
= }
=}index 92bc87e..5d904c9 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -8,7 +8,7 @@ use crate::names::{DISTRICT_NAMES, DISTRICT_PREFIXES};
=use crate::parcels::{setup_parcels, Parcel};
=use crate::population::Person;
=use crate::roads::{RoadPlan, RoadsSystems};
-use crate::SimulationParameters;
+use crate::simulation::SimulationParameters;
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;index f75a053..a470802 100644
--- a/src/ground.rs
+++ b/src/ground.rs
@@ -1,4 +1,5 @@
-use crate::SimulationParameters;
+use crate::major_state::MajorState;
+use crate::simulation::{self, SimulationParameters};
=use bevy::prelude::*;
=use std::f32::consts::FRAC_PI_2;
=
@@ -6,7 +7,10 @@ pub struct GroundPlugin;
=
=impl Plugin for GroundPlugin {
= fn build(&self, app: &mut App) {
- app.add_systems(Startup, setup_ground);
+ app.add_systems(
+ OnEnter(MajorState::GettingReady),
+ setup_ground.after(simulation::get_ready),
+ );
= }
=}
=index 762f53c..72bfc09 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -18,7 +18,7 @@ use crate::population::Residence;
=use crate::population::Savings;
=use crate::population::Sex;
=use crate::ron_export;
-use crate::SimulationParameters;
+use crate::simulation::SimulationParameters;
=use bevy::prelude::*;
=use bevy_egui::egui;
=use bevy_egui::egui::epaint::Shadow;
@@ -149,7 +149,8 @@ fn paint_simulation_progress_bar(
= date: Res<Date>,
= mut auto_advance: ResMut<AutoAdvanceSimulation>,
= mut commands: Commands,
- simulation: Res<SimulationParameters>,
+ simulation_parameters: Res<SimulationParameters>,
+ runtime_parameters: Res<Configuration>,
= systems: Res<DateSystems>,
=) {
= egui::TopBottomPanel::bottom("simulation_controls")
@@ -197,7 +198,13 @@ fn paint_simulation_progress_bar(
= ui.end_row();
=
= ui.horizontal(|ui| {
- let progress = date.fraction_of_simulation(&simulation);
+ let total_duration =
+ Duration::from_years(runtime_parameters.duration as f32);
+ let simulated_duration = Duration::new(
+ &simulation_parameters.beginning(&runtime_parameters),
+ &date.0,
+ );
+ let progress = simulated_duration / total_duration;
= ui.add(
= ProgressBar::new(progress)
= .text(date.0.year().to_string())
@@ -213,6 +220,7 @@ fn paint_replay_controls(
= mut contexts: EguiContexts,
= history: Res<History>,
= snapshots: Res<Snapshots>,
+ simulation_parameters: Res<SimulationParameters>,
= mut slider_value: ResMut<DateSliderValue>,
= mut time: ResMut<Time<Virtual>>,
=) {
@@ -269,7 +277,9 @@ fn paint_replay_controls(
=
= let export_button = ui.add(egui::Button::new("⏏").min_size(min_button_size));
= if export_button.clicked() {
- ron_export::export(history.as_ref());
+ // TODO: Can I avoid cloning data to save a simulation?
+ // Saving needs read-only access, so it should be possible.
+ ron_export::export(history.clone(), simulation_parameters.clone());
= }
= });
=
@@ -299,9 +309,11 @@ fn paint_replay_controls(
=fn update_slider_value(
= mut slider_value: ResMut<DateSliderValue>,
= date: Res<Date>,
- simulation: Res<SimulationParameters>,
+ history: Res<History>,
=) {
- slider_value.bypass_change_detection().0 = date.months_simulated(&simulation) as f64;
+ let beginning = history.events.first().unwrap().date;
+ let duration = Duration::new(&beginning, &date.0);
+ slider_value.bypass_change_detection().0 = duration.months as f64;
=}
=
=fn apply_slider_value(index 81a8ca1..e374204 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -6,8 +6,6 @@ use crate::districts::NewDistrictEstablished;
=use crate::major_state::{MajorState, ReadyPredicates};
=use crate::population::{Died, Immigrated, MovedIn, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
-use crate::simulation::SimulationAssetHandle;
-use crate::Simulation;
=use bevy::ecs::system::{RunSystemOnce, SystemId};
=use bevy::prelude::*;
=use serde::{Deserialize, Serialize};
@@ -22,7 +20,6 @@ impl Plugin for HistoryPlugin {
= .init_resource::<Snapshots>()
= .add_systems(Startup, setup_history_systems)
= .add_systems(Startup, register_ready_predicates)
- .add_systems(OnEnter(MajorState::GettingReady), import_simulation)
= .add_systems(
= PreUpdate,
= take_snapshot.run_if(
@@ -55,24 +52,6 @@ fn history_ready(future: Res<Future>) -> bool {
= future.events.is_empty()
=}
=
-pub fn import_simulation(
- handle: Option<Res<SimulationAssetHandle>>,
- assets: Res<Assets<Simulation>>,
- mut future: ResMut<Future>,
-) {
- if let Some(handle) = handle {
- let simulation = assets.get(handle.0.clone()).unwrap();
-
- // TODO: Start snapshots recalculation here
- info!(
- "Loaded the simulation asset with {} events",
- simulation.events.len()
- );
- // TODO: Don't clone the events from loaded simulation - move them!
- future.events = simulation.events.clone();
- }
-}
-
=#[derive(Resource, Default, Clone, Debug, Serialize, Deserialize)]
=pub struct History {
= pub events: Vec<EventLogEntry>,index d61d7fd..3bddefb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -16,65 +16,3 @@ pub mod roads;
=pub mod ron_export;
=pub mod simulation;
=pub mod sun;
-
-use bevy::prelude::*;
-use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
-use bevy_inspector_egui::prelude::*;
-use day_month::DayMonth;
-use day_month::HOUR;
-pub use simulation::Simulation;
-
-// TODO: Use configuration to set SimulationParameters.
-// Store them in and restore from Simulation asset.
-/// Immutable parameters of a simulation.
-///
-/// Changing them in the middle of a running simulation will have unspecified
-/// and probably horrible effects. If this parameters are changed, the
-/// simulation has to be re-run.
-#[derive(Debug, Resource, Reflect, InspectorOptions)]
-#[reflect(Resource)]
-pub struct SimulationParameters {
- /// How big is the landmass on which Otterhide is built
- pub land_radius: f32,
- /// Extra distance from the edge of landmass to the sun
- pub sun_gap: f32,
- /// Elevate the canter of sun's orbit for longer days
- pub sun_elevation: f32,
- // IDEA: We can simplify some calculations by using this value only for Display
- pub first_year: i32,
- // The length of the simulation
- pub years: u16,
- /// Day-months per one second of real time during simulation
- #[inspector(min = 2, max = 255, display = NumberDisplay::Slider)]
- pub frames_per_day_month: u8,
- /// What time the first frame of the day starts
- ///
- /// 0.0 - midnight. 0.5 - noon
- pub frame_offset: f32,
-}
-
-impl Default for SimulationParameters {
- fn default() -> Self {
- Self {
- land_radius: 2000.,
- sun_gap: 200.,
- sun_elevation: 600.,
- first_year: 1865,
- #[cfg(debug_assertions)]
- years: 3,
- #[cfg(not(debug_assertions))]
- years: 150,
- frame_offset: 2.0 * HOUR,
- frames_per_day_month: 6,
- }
- }
-}
-
-impl SimulationParameters {
- pub fn beginning(&self) -> DayMonth {
- *DayMonth::new(self.first_year).advance(self.frame_offset)
- }
- pub fn end(&self) -> DayMonth {
- *DayMonth::new(self.first_year + self.years as i32).advance(self.frame_offset)
- }
-}index 8e8ecd9..b5b16bf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,7 +18,6 @@ use otterhide::population::PopulationPlugin;
=use otterhide::roads::RoadsPlugin;
=use otterhide::simulation::SimulationLoadingPlugin;
=use otterhide::sun::SunPlugin;
-use otterhide::SimulationParameters;
=
=fn main() {
= App::new()
@@ -32,8 +31,6 @@ fn main() {
= ..default()
= }))
= .register_type::<Uuid>()
- .register_type::<SimulationParameters>()
- .init_resource::<SimulationParameters>()
= .add_plugins(ConfigurationPlugin)
= .add_plugins(MajorStatePlugin)
= .add_plugins(SimulationLoadingPlugin)index 6db9c47..33e5bb4 100644
--- a/src/major_state.rs
+++ b/src/major_state.rs
@@ -1,6 +1,7 @@
+use crate::configuration::Configuration;
=use crate::date::{Date, NewMonth};
=use crate::simulation::SimulationAssetHandle;
-use crate::SimulationParameters;
+use crate::simulation::SimulationParameters;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy::utils::HashSet;
@@ -123,12 +124,13 @@ pub fn exit_simulating_state(
= date: Res<Date>,
= mut state: ResMut<NextState<MajorState>>,
= current_state: Res<State<MajorState>>,
- simulation: Res<SimulationParameters>,
+ simulation_parameters: Res<SimulationParameters>,
+ runtime_parameters: Res<Configuration>,
=) {
= let current_state = current_state.get();
= let next_state = MajorState::Exploring;
=
- if date.0 > simulation.end() {
+ if date.0 > simulation_parameters.end(&runtime_parameters) {
= info!("Simulation complete. Switching from {current_state:?} to {next_state:?}");
= state.set(next_state);
= }index 57c156b..627d3ea 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -5,7 +5,7 @@ use crate::day_month::{DayMonth, Duration};
=use crate::history::Snapshot;
=use crate::major_state::MajorState;
=use crate::names::{FEMALE_NAMES, LAST_NAMES, MALE_NAMES};
-use crate::SimulationParameters;
+use crate::simulation::SimulationParameters;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy::utils::{HashMap, Uuid};
@@ -15,6 +15,7 @@ use rand::distributions::Standard;
=use rand::prelude::*;
=use serde::{Deserialize, Serialize};
=use std::fmt::Display;
+use std::ops::Neg;
=
=pub struct PopulationPlugin;
=
@@ -181,8 +182,13 @@ impl Distribution<PersonDescription> for PersonsDistribution {
= third: None,
= }
= };
- let age: f32 = rng.gen_range(1.0..120.0);
- let birth_date = self.current_date.clone().advance(-age).to_owned().into();
+ let age = Duration::from_months(rng.gen_range(1.0..120.0));
+ let birth_date = self
+ .current_date
+ .clone()
+ .advance(&age.neg())
+ .to_owned()
+ .into();
= let savings = rng.gen_range(100.0..10000.0).into();
= PersonDescription {
= id,
@@ -409,7 +415,7 @@ impl Snapshot for PopulationSnapshot {
= &BirthDate,
= &Savings,
= Option<&Residence>,
- ), With<Person>>();
+ ), Without<Dead>>();
= // TODO: Can I satisfy the borrow checker without collecting the iterator?
= let persons = query
= .iter(world)index 471ac74..eb59ab3 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,10 +1,10 @@
=use crate::coordinates::{Coordinates, Direction};
=
-use crate::history;
=use crate::history::Snapshot;
=use crate::major_state::MajorState;
=use crate::major_state::PreloadedAssets;
-use crate::SimulationParameters;
+use crate::simulation;
+use crate::simulation::SimulationParameters;
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -25,8 +25,7 @@ impl Plugin for RoadsPlugin {
= .add_systems(Startup, register_road_systems)
= .add_systems(
= OnEnter(MajorState::GettingReady),
- // TODO: Avoid coupling with history systems
- lay_initial_roads.before(history::take_snapshot),
+ lay_initial_roads.after(simulation::get_ready),
= );
= }
=}index 25a8efc..8792199 100644
--- a/src/ron_export.rs
+++ b/src/ron_export.rs
@@ -1,11 +1,15 @@
=use crate::history::History;
+use crate::simulation::{Simulation, SimulationParameters};
=use bevy::log::info;
=use ron::ser::to_string_pretty;
=
-pub(crate) fn export(history: &History) {
- let exported = to_string_pretty(history, ron::ser::PrettyConfig::default()).unwrap();
+pub(crate) fn export(history: History, parameters: SimulationParameters) {
+ let simulation = Simulation {
+ events: history.events,
+ parameters,
+ };
+ let exported = to_string_pretty(&simulation, ron::ser::PrettyConfig::default()).unwrap();
= let file_name = "simulation.ron";
-
= info!("Saving the simulation to {file_name}");
= save_file(file_name, exported.as_str());
=}index 8039eb8..41e44e5 100644
--- a/src/simulation.rs
+++ b/src/simulation.rs
@@ -1,8 +1,11 @@
=use crate::configuration::Configuration;
-use crate::history::EventLogEntry;
-use crate::major_state::PreloadedAssets;
+use crate::day_month::{DayMonth, Duration};
+use crate::history::{EventLogEntry, Future};
+use crate::major_state::{MajorState, PreloadedAssets};
+use bevy::asset::AssetLoader;
=use bevy::asset::AsyncReadExt;
-use bevy::{asset::AssetLoader, prelude::*};
+use bevy::prelude::*;
+use bevy_inspector_egui::InspectorOptions;
=use serde::{Deserialize, Serialize};
=use thiserror::Error;
=
@@ -12,15 +15,83 @@ impl Plugin for SimulationLoadingPlugin {
= fn build(&self, app: &mut App) {
= app.init_asset::<Simulation>()
= .init_asset_loader::<SimulationLoader>()
- .add_systems(Startup, load_simulation);
+ .add_systems(Startup, load_simulation)
+ .add_systems(OnEnter(MajorState::GettingReady), get_ready);
= }
=}
=
+pub fn get_ready(world: &mut World) {
+ world.resource_scope(|world, assets: Mut<Assets<Simulation>>| {
+ let handle = world.get_resource::<SimulationAssetHandle>();
+
+ if let Some(handle) = handle {
+ let simulation = assets.get(handle.0.clone()).unwrap();
+ world.insert_resource(simulation.parameters.clone());
+ world.insert_resource(Future {
+ events: simulation.events.clone(),
+ });
+ } else {
+ world.init_resource::<SimulationParameters>();
+ world.init_resource::<Future>();
+ }
+ });
+}
+
=/// An asset storing a simulation that can be replayed and explored
=#[derive(Asset, Debug, Deserialize, Serialize, TypePath)]
=pub struct Simulation {
= // TODO: Save other simulation parameters, like duration
= pub events: Vec<EventLogEntry>,
+ pub parameters: SimulationParameters,
+}
+
+/// Immutable parameters of a simulation.
+///
+/// Changing them in the middle of a running simulation will have unspecified
+/// and probably horrible effects. If this parameters are changed, the
+/// simulation has to be re-run.
+#[derive(Debug, Clone, Resource, InspectorOptions, Serialize, Deserialize)]
+pub struct SimulationParameters {
+ /// How big is the landmass on which Otterhide is built
+ pub land_radius: f32,
+ /// Extra distance from the edge of landmass to the sun
+ pub sun_gap: f32,
+ /// Elevate the canter of sun's orbit for longer days
+ pub sun_elevation: f32,
+ /// Day-months per one second of real time during simulation
+ pub frames_per_day_month: u8,
+ /// What time the first frame of the day starts
+ pub frame_offset: Duration,
+}
+
+impl Default for SimulationParameters {
+ fn default() -> Self {
+ Self {
+ land_radius: 2000.,
+ sun_gap: 200.,
+ sun_elevation: 600.,
+ frame_offset: Duration::from_hours(2.0),
+ frames_per_day_month: 6,
+ }
+ }
+}
+
+impl SimulationParameters {
+ /// The first moment to simulate
+ ///
+ /// NOTE: Do not use this in exploration! Take the date of the first event
+ /// instead.
+ pub fn beginning(&self, configuration: &Configuration) -> DayMonth {
+ *DayMonth::new(configuration.beginning as i32).advance(&self.frame_offset)
+ }
+ /// The last moment to simulate
+ ///
+ /// NOTE: Do not use this in exploration! Take the date of the last event
+ /// instead.
+ pub fn end(&self, configuration: &Configuration) -> DayMonth {
+ *DayMonth::new(configuration.beginning as i32 + configuration.duration as i32)
+ .advance(&self.frame_offset)
+ }
=}
=
=fn load_simulation(index d0c0b0f..5cd6110 100644
--- a/src/sun.rs
+++ b/src/sun.rs
@@ -1,5 +1,7 @@
=use crate::date::Date;
-use crate::SimulationParameters;
+use crate::major_state::MajorState;
+use crate::simulation;
+use crate::simulation::SimulationParameters;
=use bevy::prelude::*;
=use std::f32::consts::PI;
=use std::ops::{Mul, Sub};
@@ -8,8 +10,14 @@ pub struct SunPlugin;
=
=impl Plugin for SunPlugin {
= fn build(&self, app: &mut App) {
- app.add_systems(Startup, setup_sunlight)
- .add_systems(Update, (move_sun, update_sunlight_color, draw_gizmos));
+ app.add_systems(
+ OnEnter(MajorState::GettingReady),
+ setup_sunlight.after(simulation::get_ready),
+ )
+ .add_systems(
+ Update,
+ (move_sun, update_sunlight_color, draw_gizmos).run_if(resource_exists::<Date>),
+ );
= }
=}
=Fix: residents not restored from snapshots
On by
index a4268ed..7fe072e 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -443,6 +443,9 @@ fn construct_building(
= if residents.len() < capacity as usize {
= entity.insert(HasSpareCapacity);
= };
+ if residents.is_empty().not() {
+ entity.insert(Residents(residents));
+ }
=
= let entity = entity.id();
=Fix crashing with population inspector
On by
Initially there is no Date resource, and the inspector depends on it. So now it is only painted once there is Date.
index 72bfc09..942fef2 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -66,7 +66,7 @@ impl Plugin for HudPlugin {
= getting_ready_system_set.run_if(in_state(MajorState::GettingReady)),
= simulation_system_set.run_if(in_state(MajorState::Simulating)),
= exploration_system_set.run_if(in_state(MajorState::Exploring)),
- central_panel_system_set,
+ central_panel_system_set.run_if(resource_exists::<Date>),
= )
= .chain(),
= );Prevent the stroboscopic effect while simulating
On by
Now in simulation state the day and night cycle lasts a year (also in getting ready, although the camera is off, so can't be seen)
index 5cd6110..b01e487 100644
--- a/src/sun.rs
+++ b/src/sun.rs
@@ -4,7 +4,7 @@ use crate::simulation;
=use crate::simulation::SimulationParameters;
=use bevy::prelude::*;
=use std::f32::consts::PI;
-use std::ops::{Mul, Sub};
+use std::ops::{Div, Mul, Sub};
=
=pub struct SunPlugin;
=
@@ -51,12 +51,24 @@ fn move_sun(
= mut sun: Query<&mut Transform, With<Sun>>,
= date: Res<Date>,
= settings: Res<SimulationParameters>,
+ state: Res<State<MajorState>>,
=) {
= // TODO: Take elevation into account, so that the gap is actually between the edge of land and the sun
= let orbit = settings.land_radius + settings.sun_gap;
=
- let daytime = date.0.time_of_day();
- let angle = daytime * 2.0 * PI - PI;
+ let angle = if *state == MajorState::Exploring {
+ // In Exploration state sun goes around every day-month
+ let daytime = date.0.time_of_day();
+ daytime * 2.0 * PI - PI
+ } else {
+ // In simulation or getting ready, sun goes around every year
+ // In those states time progresses on rapidly (as fast as possible), so
+ // day-monthly sun cycle would look like a stroboscope.
+ let season = (date.0.month() as usize as f32 + date.0.time_of_day())
+ .rem_euclid(12.0)
+ .div(12.0);
+ season * 2.0 * PI - PI
+ };
= // TODO: Lean the orbit more in winters.
= let rotation = Quat::from_rotation_x(angle) * Quat::from_rotation_z(1.0);
= let translation = rotation.mul_vec3(Vec3::Y) * orbit;
@@ -66,8 +78,19 @@ fn move_sun(
= transform.look_at(Vec3::ZERO, Vec3::Y);
=}
=
-fn update_sunlight_color(mut sun: Query<&mut DirectionalLight, With<Sun>>, date: Res<Date>) {
- let daytime = date.0.time_of_day();
+fn update_sunlight_color(
+ mut sun: Query<&mut DirectionalLight, With<Sun>>,
+ date: Res<Date>,
+
+ state: Res<State<MajorState>>,
+) {
+ let daytime = if *state == MajorState::Exploring {
+ date.0.time_of_day()
+ } else {
+ (date.0.month() as usize as f32 + date.0.time_of_day())
+ .rem_euclid(12.0)
+ .div(12.0)
+ };
= let sine = daytime.sub(0.25).mul(2.0 * PI).sin();
= let hue = 10.0 + (sine + 0.5) * 30.0;
= let lightness = (sine + 1.0) * 0.1 + 0.75; // between 0.75 and 0.95Don't display sun gizmo in production
On by
I'd like to have a nice disk, but for now let's just hide the funny gizmo.
index b01e487..8fff634 100644
--- a/src/sun.rs
+++ b/src/sun.rs
@@ -27,6 +27,7 @@ struct Sun;
=fn draw_gizmos(mut gizmos: Gizmos, sun: Query<(&Transform, &DirectionalLight), With<Sun>>) {
= let (position, light) = sun.single();
=
+ #[cfg(degug)]
= gizmos.sphere(position.translation, Quat::default(), 100., light.color);
=}
=Setup Git LFS, add 2 saved simulations
On by
new file mode 100644
index 0000000..df9df80
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+assets/*.ron filter=lfs diff=lfs merge=lfs -textindex 254617d..ea63fe9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,3 @@
=/.cargo-home/
=/data/
=*.blend1
-
-# TODO: Do not ignore pre-recorded simulations once we got their size under control
-assets/simulation*.ronnew file mode 100644
index 0000000..c16b90e
--- /dev/null
+++ b/assets/simulation-1840+150.ron
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c4132c60f366b51b2b28342b78b0fea9ae0ced6ee3aa6b2dfaa48c3a48cb2b35
+size 40918044new file mode 100644
index 0000000..319aca3
--- /dev/null
+++ b/assets/simulation-1920+60.ron
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f53ebf539524feb35dcf3da9078f541456d7eb10d099b48a2be6384fe04c628
+size 16987895