Week 13 of 2024
Development log of Otterhide
46 items
- Add a suffix to the school in district-220x460.001
- Give districts floral names
- Fix new districts sometimes out of place
- Let the buildings have addresses
- Organize components of a Person into a bundle
- Implement PersonBundle + Standard Distribution
- Build one house at a time with probability
- Implement a simple UI showing the list of people
- Implement the population snapshot logic
- Add Macaco to the list of last names
- Move Snapshot trait to the history module
- Refactor population
- Split immigration system into plan and implement
- Implement immigrated events capturing and replay
- Implement a "stable" person Id and register
- Implement BuildingId component
- Implement BuildingsRegister resource
- Make the simulation 150 year long in production
- Implement residency relation
- Implement the capacity and overcrowded components
- Keep track of housing capacity
- Use Snapshot trait for buildings
- Create 2 new districts with paths
- Added district of 300x540 and added roads and fixed buildings of 2 older districts
- Added lake, added trees and fountains, added 2 big houses
- Added second lake and extra trees.
- Added 180x180 tile, added hill and lake a bit deeper. Fixed small bug.
- Use Snapshot trait for roads
- Re-work the capacity and overcrowding logic
- Make sure future events don't leak to the past
- Revert simulation duration to 2 years
- Remove an unused variable
- Use Snapshot trait for districts
- Use Snapshot trait for date (the last one)
- Fix a variable name
- Implement history export in RON format
- Extract ron export to own module
- Implement exported file download
- Redeployed ending of internal paths to follow a 60m grid (starting from road corners)
- POC loading of a simulation
- Fix "moving in" runtime error
- Implement save to a file function for desktop
- Setup otterhide-to-sqlite export program
- POC: Generate a database using SQLx and dump it
- Remove unused import
- SQLite export will accept input and output arguments
Add a suffix to the school in district-220x460.001
On by
Without it it wasn't recognized as a parcel.
index 96a0c9f..277bc87 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex 0c362f1..10b22bd 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differGive districts floral names
On by
Each new district will have a name based on a plant, like "Redwood Ridge" or "Oak Overlook". The names always have two words with the same first letter (thanks, FastGPT). In case of a collision (name already in use), a random prefix (one of "New", "Lower", "Upper", "East", "West", "South", "North") will be applied. There is no logic to the directional prefixes, so "East Elm Estates" might be placed to the west of "West Elm Estates". Also, the first letter rule doesn't apply to prefixes. If a base name and all prefixed variation are used, the district won't be established. There is basically negligible chance of that happening, and even if so, the next time different base name will be tried and will probably work. So this brings a bit of extra randomness to the urban planning. Think about it as a bikeshedding at the townhall meeting :D
index e820bb0..9b5fb24 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,20 +1,21 @@
=use crate::buildings::{Building, BuildingVariant};
=use crate::coordinates::{Coordinate, Coordinates, Direction, Latitude, Longitude};
=use crate::date::SimulationFrameCounter;
+use crate::names::{DISTRICT_NAMES, DISTRICT_PREFIXES};
=use crate::population::Person;
=use crate::roads::{RoadPlan, RoadsSystems};
=use crate::{GameState, PreloadedAssets, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
-use bevy::utils::HashMap;
+use bevy::utils::{HashMap, HashSet};
=use itertools::Itertools;
-use rand::seq::IteratorRandom;
+use rand::seq::{IteratorRandom, SliceRandom};
=use rand::thread_rng;
=use regex::Regex;
=use std::borrow::Borrow;
=use std::f32::consts::FRAC_PI_2;
-use std::iter;
+use std::iter::{self, once};
=use std::ops::{AddAssign, Not};
=use std::str::FromStr;
=
@@ -183,9 +184,9 @@ impl DistrictBundle {
= ..default()
= };
= Self {
+ name: description.name.clone(),
= description,
= scene,
- name: Name::new("District"),
= marker: District,
= }
= }
@@ -296,6 +297,9 @@ pub struct DistrictDescription {
=
= /// Which model to use
= pub variant: DistrictVariant,
+
+ /// The name of this district
+ pub name: Name,
=}
=
=#[derive(Component, Debug, Clone, Copy)]
@@ -339,8 +343,9 @@ impl From<isize> for Rotation {
=}
=
=impl DistrictDescription {
- pub fn new(origin: Coordinates, variant: DistrictVariant) -> Self {
+ pub fn new(name: String, origin: Coordinates, variant: DistrictVariant) -> Self {
= Self {
+ name: Name::new(name),
= origin,
= variant,
= rotation: Rotation::None,
@@ -508,6 +513,12 @@ fn plan_new_districts(
=
= // TODO: Use some smarter heuristics to choose the district
= let variant = scenes.0.keys().choose(&mut thread_rng()).unwrap();
+ let Some(name) = random_district_name(&districts) else {
+ warn!("Couldn't find a name for a new district. It might work next time.");
+ return;
+ };
+
+ // TODO: Ensure names are unique
=
= let corners = districts
= .iter()
@@ -516,7 +527,7 @@ fn plan_new_districts(
= .unique();
=
= let candidates = corners
- .map(|corner| DistrictDescription::new(corner, variant.clone()))
+ .map(|corner| DistrictDescription::new(name.to_string(), corner, variant.clone()))
= .flat_map(|candidate| {
= [
= candidate.clone().rotate(Rotation::None).to_owned(),
@@ -581,6 +592,25 @@ fn plan_new_districts(
= new_districts.send(NewDistrictEstablished(selected));
=}
=
+/// Get a random, unique name for a new district
+///
+/// NOTE: This function has a minuscule chance of returning None, even when
+/// there are possible names. It's a feature?
+fn random_district_name(districts: &Query<&DistrictDescription>) -> Option<String> {
+ let used_names: HashSet<String> = districts
+ .iter()
+ .map(|district| district.name.to_string())
+ .collect();
+ let base_name = DISTRICT_NAMES.iter().choose(&mut thread_rng()).unwrap();
+ let mut prefixes = DISTRICT_PREFIXES.to_owned();
+ prefixes.shuffle(&mut thread_rng());
+ let variations = prefixes
+ .iter()
+ .map(|prefix| format!("{prefix} {base_name}"));
+ let mut candidates = once(base_name.to_string()).chain(variations);
+ candidates.find(|name| used_names.contains(name).not())
+}
+
=/// Process NewDistrictEstablished events
=///
=/// The events might be coming from establish_new_districts or replay_historical_events
@@ -660,6 +690,7 @@ mod district_tests {
= #[test]
= fn measurements_test() {
= let district = DistrictDescription::new(
+ "a".into(),
= Coordinates::new(Coordinate::new(-3), Coordinate::new(-2)),
= DistrictVariant {
= length: 13,
@@ -711,7 +742,7 @@ mod district_tests {
= width: 20,
= number: 0,
= };
- let district = DistrictDescription::new(Coordinates::default(), variant);
+ let district = DistrictDescription::new("a".into(), Coordinates::default(), variant);
= let rect = Rect::from(&district);
= assert_eq!(rect.min, Vec2::ZERO);
= assert_eq!(rect.max, Vec2::new(100.0, 200.0));
@@ -722,6 +753,7 @@ mod district_tests {
= number: 0,
= };
= let district = DistrictDescription::new(
+ "b".into(),
= Coordinates::new(
= Coordinate::<Longitude>::new(10),
= Coordinate::<Latitude>::new(5),
@@ -740,7 +772,7 @@ mod district_tests {
= width: 20,
= number: 0,
= };
- let district = DistrictDescription::new(Coordinates::default(), variant);
+ let district = DistrictDescription::new("a".into(), Coordinates::default(), variant);
= assert_eq!(district.center(), Vec3::new(50.0, 0.0, 100.0));
=
= let variant = DistrictVariant {
@@ -749,6 +781,7 @@ mod district_tests {
= number: 0,
= };
= let district = DistrictDescription::new(
+ "a".into(),
= Coordinates::new(
= Coordinate::<Longitude>::new(10),
= Coordinate::<Latitude>::new(5),
@@ -776,6 +809,7 @@ mod district_tests {
= assert!(a.intersect(b).is_empty());
=
= let a = DistrictDescription {
+ name: Name::new("a"),
= origin: Coordinates::new(
= Coordinate::<Longitude>::new(-60),
= Coordinate::<Latitude>::new(-49),
@@ -788,6 +822,7 @@ mod district_tests {
= rotation: Rotation::CW270,
= };
= let b = DistrictDescription {
+ name: Name::new("b"),
= origin: Coordinates::new(
= Coordinate::<Longitude>::new(-60),
= Coordinate::<Latitude>::new(-18),index e371c8e..0b840d1 100644
--- a/src/names.rs
+++ b/src/names.rs
@@ -494,3 +494,93 @@ const MALE_NAMES: NamesList = &[
= "Xavier",
= "Zachary",
=];
+
+pub const DISTRICT_NAMES: NamesList = &[
+ "Allspice Alley",
+ "Aloe Alley",
+ "Anise Acres",
+ "Aspen Acres",
+ "Aster Acres",
+ "Bamboo Bluff",
+ "Basil Bluff",
+ "Beech Bluff",
+ "Bluebell Bluff",
+ "Cactus Canyon",
+ "Camellia Cove",
+ "Cardamom Commons",
+ "Chestnut Commons",
+ "Chrysanthemum Crossing",
+ "Cilantro Cove",
+ "Cinnamon Crossing",
+ "Clove Court",
+ "Clover Commons",
+ "Clover Cove",
+ "Coriander Cove",
+ "Cumin Creek",
+ "Cypress Creek",
+ "Daffodil Downs",
+ "Dahlia Dell",
+ "Daisy Dale",
+ "Daisy Dell",
+ "Dandelion Downs",
+ "Dill Dell",
+ "Elm Estates",
+ "Eucalyptus Estates",
+ "Fennel Falls",
+ "Fern Falls",
+ "Fernglen Forest",
+ "Fernwood Falls",
+ "Fir Forest",
+ "Geranium Gardens",
+ "Ginger Gardens",
+ "Ginseng Glen",
+ "Hazel Heights",
+ "Hibiscus Hollow",
+ "Hickory Hills",
+ "Hyacinth Hills",
+ "Iris Islands",
+ "Ivy Inn",
+ "Jasmine Junction",
+ "Juniper Junction",
+ "Lavender Lane",
+ "Lilac Lane",
+ "Lily Lake",
+ "Lotus Loop",
+ "Magnolia Manse",
+ "Maple Meadow",
+ "Marigold Manor",
+ "Mint Meadow",
+ "Mossy Meadow",
+ "Mustard Manor",
+ "Nutmeg Knoll",
+ "Oak Overlook",
+ "Orchid Oaks",
+ "Oregano Oaks",
+ "Palm Park",
+ "Pansy Park",
+ "Pansy Place",
+ "Parsley Place",
+ "Peony Point",
+ "Pepper Pass",
+ "Petunia Place",
+ "Pine Point",
+ "Poppy Place",
+ "Redwood Ridge",
+ "Rosemary Ridge",
+ "Rosewood Ridge",
+ "Saffron Shores",
+ "Sage Summit",
+ "Sunflower Shores",
+ "Thyme Terrace",
+ "Tulip Terrace",
+ "Violet Vale",
+ "Walnut Woods",
+ "Wasabi Way",
+ "Willow Woods",
+ "Wisteria Walk",
+ "Yucca Yards",
+ "Zinnia Zones",
+];
+
+pub const DISTRICT_PREFIXES: NamesList =
+ &["New", "Lower", "Upper", "East", "West", "South", "North"];Fix new districts sometimes out of place
On by
There was a bug in width and length measurement of rotated district candidates. Basically we should never access width and length fields of a DistrictVariant, instead always use the DistrictDescription::width and ::length methods, that correctly apply rotation.
index 9b5fb24..6d0b7f0 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -538,22 +538,20 @@ fn plan_new_districts(
= .into_iter()
= })
= .flat_map(|candidate| {
- let DistrictDescription { variant, .. } = candidate.clone();
- let DistrictVariant { length, width, .. } = variant;
= [
= candidate.clone(),
= candidate
= .clone()
- .shift(width as i32, &Direction::North)
+ .shift(candidate.width() as i32, &Direction::North)
= .to_owned(),
= candidate
= .clone()
- .shift(width as i32, &Direction::North)
- .shift(length as i32, &Direction::West)
+ .shift(candidate.width() as i32, &Direction::North)
+ .shift(candidate.length() as i32, &Direction::West)
= .to_owned(),
= candidate
= .clone()
- .shift(length as i32, &Direction::West)
+ .shift(candidate.length() as i32, &Direction::West)
= .to_owned(),
= ]
= .into_iter()
@@ -583,6 +581,7 @@ fn plan_new_districts(
= .any(|existing| existing.overlaps(candidate))
= .not()
= });
+
= let Some(selected) = candidates.choose(&mut thread_rng()) else {
= info!("Can't find a suitable spot for a new district!");
= return;Let the buildings have addresses
On by
The address is composed of district name and parcel number, like "23 Clover Cove" or "178 Lower Elm Estates".
To make it work I'm cleverly taking advantage of the fact that parcel entities (being part of a scene) are children of a district entity. So I can just assign them numbers as soon as districts asset is loaded (in a newly separated setup_parcels function), and when spawning buildings lookup the parent district and it's name to compose the building address. It's inside the Name entity (because address is the name of a building).
While making it work I separated parcels relate logic to own module and plugin. There are also two new components:
-
BuildingClass
Assigned to both Parcel and Building entities.
-
ParcelNumber
Assigned to Parcel entities and later copied to Name component of a Building entity.
There is also the new ParcelBundle to tie all the related components together. As mentioned above, also the new setup_parcels function, however it's mostly code extracted from the good old setup_districts. It was growing too complex with too many responsibilities, so I split it.
index 334720c..749613a 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -1,5 +1,6 @@
=use crate::date::SimulationFrameCounter;
-use crate::districts::Parcel;
+use crate::districts::District;
+use crate::parcels::{Parcel, ParcelNumber};
=use crate::population::Person;
=use crate::{GameState, PreloadedAssets};
=use bevy::ecs::system::SystemId;
@@ -19,6 +20,7 @@ impl Plugin for BuildingsPlugin {
= app.add_event::<ConstructionOrder>()
= .init_resource::<BuildingScenes>()
= .register_type::<BuildingScenes>()
+ .register_type::<BuildingClass>()
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_buildings_systems)
= .add_systems(OnEnter(GameState::Simulate), setup_building_scenes)
@@ -130,6 +132,7 @@ pub struct ConstructionOrder(pub BuildingDescription);
=pub struct BuildingDescription {
= pub transform: Transform,
= pub variant: BuildingVariant,
+ pub address: String,
=}
=
=/// A tag for building entities
@@ -141,16 +144,24 @@ pub struct BuildingBundle {
= variant: BuildingVariant,
= scene: SceneBundle,
= name: Name,
+ class: BuildingClass,
= marker: Building,
=}
=
=impl BuildingBundle {
- pub fn new(variant: BuildingVariant, transform: Transform, scenes: &BuildingScenes) -> Self {
+ pub fn new(
+ address: String,
+ variant: BuildingVariant,
+ transform: Transform,
+ scenes: &BuildingScenes,
+ ) -> Self {
= let scene = scenes.0.get(&variant).unwrap().clone();
- let name = format!("Building {variant}").into();
+ let name = address.into();
+ let class = BuildingClass(variant.class.clone());
= Self {
= variant,
= name,
+ class,
= scene: SceneBundle {
= scene,
= transform,
@@ -177,6 +188,10 @@ pub struct BuildingVariant {
= pub number: u16,
=}
=
+#[derive(Component, Reflect, Debug, Clone)]
+#[reflect(Component)]
+pub struct BuildingClass(pub String);
+
=impl Display for BuildingVariant {
= fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
= let Self { class, number } = self;
@@ -206,7 +221,17 @@ impl FromStr for BuildingVariant {
=fn order_construction(
= mut build_events: EventWriter<ConstructionOrder>,
= people: Query<&Person>,
- parcels: Query<(Entity, &Parcel, &GlobalTransform)>,
+ parcels: Query<
+ (
+ Entity,
+ &GlobalTransform,
+ &Parent,
+ &ParcelNumber,
+ &BuildingClass,
+ ),
+ With<Parcel>,
+ >,
+ districts: Query<&Name, With<District>>,
= buildings: Query<&Building>,
= mut commands: Commands,
= scenes: Res<BuildingScenes>,
@@ -218,15 +243,20 @@ fn order_construction(
= let capacity = buildings.iter().count();
= let housing_shortage = population.saturating_sub(capacity);
=
- for (entity, parcel, transform) in parcels.iter().take(housing_shortage) {
- let scenes = &scenes.get_class(&parcel.class);
+ for (entity, transform, parent, number, class) in parcels.iter().take(housing_shortage) {
+ info!("Placing a building in a district {parent:?}");
+ let scenes = &scenes.get_class(&class.0);
= let variant = scenes.keys().choose(&mut rand::thread_rng()).unwrap();
+ let district_name = districts.get(**parent).unwrap().to_string();
+ let building_number = number.0.to_string();
+ let address = format!("{building_number} {district_name}");
=
= // Remove parcel
= commands.entity(entity).despawn();
=
= // Order the construction
= let description = BuildingDescription {
+ address,
= transform: transform.to_owned().into(),
= variant: variant.clone(),
= };
@@ -251,19 +281,26 @@ fn construct_building(
= mut commands: Commands,
= scenes: Res<BuildingScenes>,
=) {
- let BuildingDescription { variant, transform } = description.clone();
+ let BuildingDescription {
+ address,
+ variant,
+ transform,
+ } = description.clone();
= let Vec3 { x, z, .. } = transform.translation;
=
= debug!("Spawning a new {variant} building at ({x:.2}, {z:.2})!");
- commands.spawn(BuildingBundle::new(variant, transform, &scenes));
+ commands.spawn(BuildingBundle::new(address, variant, transform, &scenes));
=}
=
=pub type Snapshot = Vec<BuildingDescription>;
=
-pub fn take_snapshot(buildings: Query<(&BuildingVariant, &Transform), With<Building>>) -> Snapshot {
+pub fn take_snapshot(
+ buildings: Query<(&Name, &BuildingVariant, &Transform), With<Building>>,
+) -> Snapshot {
= buildings
= .iter()
- .map(|(variant, transform)| BuildingDescription {
+ .map(|(name, variant, transform)| BuildingDescription {
+ address: name.to_string(),
= variant: variant.clone(),
= transform: *transform,
= })index 6d0b7f0..63e6c02 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,7 +1,8 @@
-use crate::buildings::{Building, BuildingVariant};
+use crate::buildings::Building;
=use crate::coordinates::{Coordinate, Coordinates, Direction, Latitude, Longitude};
=use crate::date::SimulationFrameCounter;
=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};
@@ -24,7 +25,6 @@ pub struct DistrictsPlugin;
=impl Plugin for DistrictsPlugin {
= fn build(&self, app: &mut App) {
= app.add_event::<NewDistrictEstablished>()
- .register_type::<Parcel>()
= .init_resource::<DistrictScenes>()
= .add_systems(Startup, register_district_systems)
= .add_systems(Startup, setup_assets)
@@ -114,46 +114,7 @@ fn setup_districts(
=
= let scene = scenes_assets.get_mut(scene_handle.clone()).unwrap();
=
- let root = scene
- .world
- .query_filtered::<Entity, Without<Parent>>()
- .iter(&scene.world)
- .next()
- .unwrap();
-
- let mut query = scene.world.query::<(Entity, &Name, &Transform, &Parent)>();
- let parcels: Vec<(Entity, String, Transform)> = query
- .iter(&scene.world)
- .filter_map(|(entity, name, transform, parent)| {
- // Only consider direct first generation to avoid nested entities
- // with names matching the pattern being taken for parcels
- if parent.get() != root {
- debug!("Skipping indirect descendant {name}");
- return None;
- };
- // Check if the name matches pattern
- let BuildingVariant { class, .. } = name.parse().ok()?;
- (entity, class, *transform).into()
- })
- .collect_vec();
-
- for (entity, class, transform) in parcels {
- debug!("Processing parcel {entity:?}, class {class}");
-
- scene
- .world
- .spawn(SpatialBundle {
- transform,
- visibility: Visibility::Visible,
- ..default()
- })
- .insert(Parcel { class });
- if let Some(entity) = scene.world.get_entity_mut(entity) {
- entity.despawn_recursive();
- } else {
- warn!("This entity does not exist!");
- };
- }
+ setup_parcels(scene);
=
= scenes.0.insert(variant, scene_handle.clone());
= }
@@ -273,12 +234,6 @@ impl FromStr for DistrictVariant {
=#[derive(Resource, Debug, Default)]
=struct DistrictScenes(HashMap<DistrictVariant, Handle<Scene>>);
=
-#[derive(Component, Reflect, Debug)]
-#[reflect(Component)]
-pub struct Parcel {
- pub class: String,
-}
-
=/// This event represents a decision to construct a new building.
=///
=/// It will be stored in the history, and will result in spawning a new building.index a34dfb4..08de669 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -62,13 +62,14 @@ impl Display for HistoricalEvent {
= fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
= match self {
= HistoricalEvent::ConstructionOrder(ConstructionOrder(BuildingDescription {
+ address,
= transform,
= variant,
= })) => {
= let Vec3 { x, z, .. } = transform.translation;
= write!(
= f,
- "construction of a new building ({variant}) ordered at ({x:.2}, {z:.2})",
+ "construction of a new building ({variant}) ordered at {address} ({x:.2}, {z:.2})",
= )
= }
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {index bdc6db3..f9541d4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,6 +8,7 @@ mod exploration;
=mod ground;
=mod history;
=mod names;
+mod parcels;
=mod pgsql_export;
=mod population;
=mod roads;
@@ -30,6 +31,7 @@ use districts::DistrictsPlugin;
=use exploration::ExplorationPlugin;
=use ground::GroundPlugin;
=use history::HistoryPlugin;
+use parcels::ParcelsPlugin;
=use population::PopulationPlugin;
=use roads::RoadsPlugin;
=use simulation::SimulationPlugin;
@@ -53,6 +55,7 @@ fn main() {
= .add_plugins(CameraPlugin)
= .add_plugins(GroundPlugin)
= .add_plugins(RoadsPlugin)
+ .add_plugins(ParcelsPlugin)
= .add_plugins(DistrictsPlugin)
= .add_plugins(BuildingsPlugin)
= .add_plugins(DatePlugin)new file mode 100644
index 0000000..27b1d04
--- /dev/null
+++ b/src/parcels.rs
@@ -0,0 +1,89 @@
+use crate::buildings::{BuildingClass, BuildingVariant};
+use bevy::prelude::*;
+use itertools::Itertools;
+
+pub struct ParcelsPlugin;
+
+impl Plugin for ParcelsPlugin {
+ fn build(&self, app: &mut App) {
+ app.register_type::<Parcel>()
+ .register_type::<ParcelNumber>();
+ }
+}
+
+#[derive(Bundle, Debug)]
+pub struct ParcelBundle {
+ pub spatial: SpatialBundle,
+ pub class: BuildingClass,
+
+ /// In which district is this parcel?
+ pub number: ParcelNumber,
+ pub marker: Parcel,
+}
+
+#[derive(Component, Reflect, Debug)]
+#[reflect(Component)]
+pub struct ParcelNumber(pub u16);
+
+#[derive(Component, Reflect, Debug)]
+#[reflect(Component)]
+pub struct Parcel;
+
+/// Given a district scene from a GLTF asset modify it to have parcels
+///
+/// It works by replacing example buildings from the .glb file with Parcel
+/// entities, that during a simulation can be replaced with Building entities.
+pub fn setup_parcels(scene: &mut Scene) {
+ let root = scene
+ .world
+ .query_filtered::<Entity, Without<Parent>>()
+ .iter(&scene.world)
+ .next()
+ .unwrap();
+
+ let mut query = scene.world.query::<(Entity, &Name, &Transform, &Parent)>();
+ let parcels: Vec<(Entity, BuildingClass, Transform)> = query
+ .iter(&scene.world)
+ .filter_map(|(entity, name, transform, parent)| {
+ // Only consider direct first generation to avoid nested entities
+ // with names matching the pattern being taken for parcels
+ if parent.get() != root {
+ debug!("Skipping indirect descendant {name}");
+ return None;
+ };
+ // Check if the name matches pattern
+ let BuildingVariant { class, .. } = name.parse().ok()?;
+ (entity, BuildingClass(class), *transform).into()
+ })
+ .collect_vec();
+
+ let mut parcels_count = 0;
+ for (entity, class, transform) in parcels {
+ parcels_count += 1;
+ debug!("Processing parcel {parcels_count} {entity:?}, class {class:?}");
+
+ let number = ParcelNumber(parcels_count);
+
+ let spatial = SpatialBundle {
+ transform,
+ visibility: Visibility::Visible,
+ ..default()
+ };
+
+ scene
+ .world
+ .spawn(ParcelBundle {
+ spatial,
+ class,
+ number,
+ marker: Parcel,
+ })
+ .insert(Name::new("Parcel {parcel_count}"));
+
+ if let Some(entity) = scene.world.get_entity_mut(entity) {
+ entity.despawn_recursive();
+ } else {
+ warn!("This entity does not exist!");
+ };
+ }
+}index 2ce7b26..25c8d8c 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -33,6 +33,7 @@ impl Display for History {
=
= match event.event.clone() {
= HistoricalEvent::ConstructionOrder(ConstructionOrder(BuildingDescription {
+ address,
= transform,
= variant,
= })) => {
@@ -41,6 +42,7 @@ impl Display for History {
= writeln!(f, "Insert into buildings (")?;
= writeln!(f, " date,")?;
= writeln!(f, " variant,")?;
+ writeln!(f, " address,")?;
= writeln!(f, " coordinates")?;
= writeln!(f, ") values (")?;
= writeln!(
@@ -48,6 +50,7 @@ impl Display for History {
= " {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, ");")?;
= }Organize components of a Person into a bundle
On by
index 345c74a..80b0bc2 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -32,13 +32,9 @@ fn immigration(mut commands: Commands) {
= for index in 1..=number {
= let sex = random::<Sex>();
= let name = PersonName::get_random(&sex);
+ let savings = 100.0 * index as f32;
=
- commands
- .spawn(Person)
- .insert(Name::new(name.to_string()))
- .insert(sex)
- .insert(name)
- .insert(Savings(100.0 * index as f32));
+ commands.spawn(PersonBundle::new(name, sex, savings));
= }
=}
=
@@ -48,6 +44,30 @@ pub struct Person;
=#[derive(Component, Reflect)]
=pub struct Savings(f32);
=
+#[derive(Bundle)]
+pub struct PersonBundle {
+ pub name: PersonName,
+ pub sex: Sex,
+ pub savings: Savings,
+
+ // Derived from the above
+ pub display_name: Name,
+ pub marker: Person,
+}
+
+impl PersonBundle {
+ pub fn new(name: PersonName, sex: Sex, savings: f32) -> Self {
+ let display_name = name.to_string().into();
+ Self {
+ name,
+ sex,
+ savings: Savings(savings),
+ marker: Person,
+ display_name,
+ }
+ }
+}
+
=#[derive(Component, Reflect, Debug)]
=pub enum Sex {
= Male,Implement PersonBundle + Standard Distribution
On by
So we can have a streamlined and idiomatic random::
index 0b840d1..e7fbbe0 100644
--- a/src/names.rs
+++ b/src/names.rs
@@ -1,23 +1,6 @@
-use rand::{seq::SliceRandom, thread_rng};
-
-use crate::population::Sex;
-
-pub fn random_first_name(sex: &Sex) -> String {
- let names = match *sex {
- Sex::Male => MALE_NAMES,
- Sex::Female => FEMALE_NAMES,
- };
-
- names.choose(&mut thread_rng()).unwrap().to_string()
-}
-
-pub fn random_last_name() -> String {
- LAST_NAMES.choose(&mut thread_rng()).unwrap().to_string()
-}
-
=type NamesList = &'static [&'static str];
=
-const LAST_NAMES: NamesList = &[
+pub const LAST_NAMES: NamesList = &[
= "Aardvark",
= "Albatross",
= "Antelope",
@@ -184,7 +167,7 @@ const LAST_NAMES: NamesList = &[
= "Zebra",
=];
=
-const FEMALE_NAMES: NamesList = &[
+pub const FEMALE_NAMES: NamesList = &[
= "Aaliyah",
= "Abigail",
= "Adriana",
@@ -346,7 +329,7 @@ const FEMALE_NAMES: NamesList = &[
= "Zoe",
=];
=
-const MALE_NAMES: NamesList = &[
+pub const MALE_NAMES: NamesList = &[
= "Aaron",
= "Abel",
= "Adam",index 80b0bc2..934d717 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,8 +1,9 @@
=use crate::date;
=use crate::date::SimulationFrameCounter;
-use crate::names;
+use crate::names::{FEMALE_NAMES, LAST_NAMES, MALE_NAMES};
=use crate::GameState;
=use bevy::prelude::*;
+use rand::distributions::Standard;
=use rand::prelude::*;
=use std::fmt::Display;
=
@@ -29,12 +30,8 @@ fn immigration(mut commands: Commands) {
= let number = 20;
= info!("{number} people immigrated to Otterhide.");
=
- for index in 1..=number {
- let sex = random::<Sex>();
- let name = PersonName::get_random(&sex);
- let savings = 100.0 * index as f32;
-
- commands.spawn(PersonBundle::new(name, sex, savings));
+ for _index in 1..=number {
+ commands.spawn(random::<PersonBundle>());
= }
=}
=
@@ -68,6 +65,25 @@ impl PersonBundle {
= }
=}
=
+impl Distribution<PersonBundle> for Standard {
+ fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> PersonBundle {
+ let sex = rng.gen::<Sex>();
+ let first_names = match sex {
+ Sex::Male => MALE_NAMES,
+ Sex::Female => FEMALE_NAMES,
+ };
+ let name = {
+ PersonName {
+ first: first_names.choose(rng).unwrap().to_string(),
+ second: LAST_NAMES.choose(rng).unwrap().to_string(),
+ third: None,
+ }
+ };
+ let savings = rng.gen_range(100.0..10000.0);
+ PersonBundle::new(name, sex, savings)
+ }
+}
+
=#[derive(Component, Reflect, Debug)]
=pub enum Sex {
= Male,
@@ -91,16 +107,6 @@ pub struct PersonName {
= third: Option<String>,
=}
=
-impl PersonName {
- pub fn get_random(sex: &Sex) -> Self {
- Self {
- first: names::random_first_name(sex),
- second: names::random_last_name(),
- third: None,
- }
- }
-}
-
=impl Display for PersonName {
= fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
= let PersonName {Build one house at a time with probability
On by
On each frame there is a probability (equal housing shortage divided by 100) that a new house will be constructed.
index 749613a..99403db 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -9,6 +9,7 @@ use bevy::prelude::*;
=use bevy::utils::HashMap;
=use itertools::Itertools;
=use rand::seq::IteratorRandom;
+use rand::{thread_rng, Rng};
=use regex::Regex;
=use std::fmt::Display;
=use std::str::FromStr;
@@ -241,15 +242,25 @@ fn order_construction(
=
= let population = people.iter().count();
= let capacity = buildings.iter().count();
- let housing_shortage = population.saturating_sub(capacity);
-
- for (entity, transform, parent, number, class) in parcels.iter().take(housing_shortage) {
- info!("Placing a building in a district {parent:?}");
+ let housing_shortage = population.saturating_sub(capacity) as u32;
+
+ // Random chance of building a house: shortage / 100
+ let denominator = 100;
+ let numerator = housing_shortage.min(denominator);
+
+ if thread_rng().gen_ratio(numerator, denominator) {
+ let Some((entity, transform, parent, number, class)) =
+ parcels.iter().choose(&mut thread_rng())
+ else {
+ warn!("No more parcels to build house!");
+ return;
+ };
= let scenes = &scenes.get_class(&class.0);
= let variant = scenes.keys().choose(&mut rand::thread_rng()).unwrap();
= let district_name = districts.get(**parent).unwrap().to_string();
= let building_number = number.0.to_string();
= let address = format!("{building_number} {district_name}");
+ info!("Building a new house {address}");
=
= // Remove parcel
= commands.entity(entity).despawn();
@@ -317,7 +328,7 @@ fn rollback(
= commands.entity(entity).despawn_recursive();
= }
=
- for order in snapshot {
- commands.run_system_with_input(systems.construct_building, order.to_owned());
+ for description in snapshot {
+ commands.run_system_with_input(systems.construct_building, description.to_owned());
= }
=}Implement a simple UI showing the list of people
On by
We need some graphical representation of the population before developing more complex features.
index f9541d4..ae715c6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,6 +11,7 @@ mod names;
=mod parcels;
=mod pgsql_export;
=mod population;
+mod population_ui;
=mod roads;
=mod simulation;
=mod sun;
@@ -33,6 +34,7 @@ use ground::GroundPlugin;
=use history::HistoryPlugin;
=use parcels::ParcelsPlugin;
=use population::PopulationPlugin;
+use population_ui::PopulationUiPlugin;
=use roads::RoadsPlugin;
=use simulation::SimulationPlugin;
=use sun::SunPlugin;
@@ -65,6 +67,7 @@ fn main() {
= .add_plugins(SimulationPlugin)
= .add_plugins(ExplorationPlugin)
= .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>()))index 934d717..b8be661 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -39,7 +39,7 @@ fn immigration(mut commands: Commands) {
=pub struct Person;
=
=#[derive(Component, Reflect)]
-pub struct Savings(f32);
+pub struct Savings(pub f32);
=
=#[derive(Bundle)]
=pub struct PersonBundle {
@@ -100,6 +100,15 @@ impl Distribution<Sex> for rand::distributions::Standard {
= }
=}
=
+impl Display for Sex {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Sex::Male => write!(f, "♂"),
+ Sex::Female => write!(f, "♀"),
+ }
+ }
+}
+
=#[derive(Component, Reflect, Debug)]
=pub struct PersonName {
= first: String,new file mode 100644
index 0000000..f4ad832
--- /dev/null
+++ b/src/population_ui.rs
@@ -0,0 +1,54 @@
+use crate::population::Person;
+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<(&Name, &Sex, &Savings), With<Person>>) {
+ 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().count();
+ ui.heading(format!("The {count} 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.end_row();
+
+ for (name, sex, Savings(savings)) in persons.iter() {
+ ui.label(name.to_string());
+ ui.label(sex.to_string());
+ ui.label(format!("{savings:.2} Œ"));
+ ui.end_row();
+ }
+ })
+ })
+ });
+}Implement the population snapshot logic
On by
Using a new, trait based approach with full world access. Immediate benefit is that restore can be synchronous now. Using trait should also make the snapshot and restore logic more consistent across various modules.
In longer perspective I hope to decouple all the modules from history, by letting each of them register their own, distinct snapshot type. All those types would implement a common Snapshot trait. In the history I'd like to dynamically find all those types and produce snapshot values from each of them. I'm not yet sure it's possible, but this is a step toward finding out.
index 99403db..49cfc77 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -305,7 +305,7 @@ fn construct_building(
=
=pub type Snapshot = Vec<BuildingDescription>;
=
-pub fn take_snapshot(
+fn take_snapshot(
= buildings: Query<(&Name, &BuildingVariant, &Transform), With<Building>>,
=) -> Snapshot {
= buildingsindex 08de669..fa0d5b7 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -2,9 +2,12 @@ use crate::buildings::{self, BuildingDescription};
=use crate::buildings::{BuildingsSystems, ConstructionOrder};
=use crate::date::{Date, DateSystems, NewMonth};
=use crate::day_month::{DayMonth, Month};
+use crate::districts;
=use crate::districts::{DistrictsSystems, NewDistrictEstablished};
+use crate::population::PopulationSnapshot;
+use crate::population::SnapshotTrait;
=use crate::roads::{self, RoadsSystems};
-use crate::{districts, GameState};
+use crate::GameState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use std::fmt::Display;
@@ -110,6 +113,7 @@ pub struct Snapshot {
= pub buildings: buildings::Snapshot,
= pub districts: districts::Snapshot,
= pub roads: roads::Snapshot,
+ pub population: PopulationSnapshot,
=}
=
=// TODO: Find a way to coordinate with lay_initial_roads without exposing this system
@@ -136,6 +140,8 @@ pub fn take_snapshot(world: &mut World) {
= world.run_system(system).unwrap()
= };
=
+ let population = PopulationSnapshot::capture(world);
+
= let timestamp: Timestamp = date.into();
=
= let snapshot = Snapshot {
@@ -143,6 +149,7 @@ pub fn take_snapshot(world: &mut World) {
= buildings,
= districts,
= roads,
+ population,
= };
=
= world.resource_scope(|_, mut history: Mut<History>| {
@@ -178,6 +185,7 @@ fn rollback(In(snapshot): In<Snapshot>, world: &mut World) {
= .run_system_with_input(system, snapshot.buildings)
= .unwrap();
= }
+ snapshot.population.restore(world);
=
= let events = world.resource_scope(|_, history: Mut<History>| history.events.clone());
=index b8be661..e334e96 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -3,6 +3,8 @@ use crate::date::SimulationFrameCounter;
=use crate::names::{FEMALE_NAMES, LAST_NAMES, MALE_NAMES};
=use crate::GameState;
=use bevy::prelude::*;
+use derive_more::From;
+use itertools::Itertools;
=use rand::distributions::Standard;
=use rand::prelude::*;
=use std::fmt::Display;
@@ -38,7 +40,7 @@ fn immigration(mut commands: Commands) {
=#[derive(Component, Debug, Reflect)]
=pub struct Person;
=
-#[derive(Component, Reflect)]
+#[derive(Component, Reflect, Clone, Debug, From)]
=pub struct Savings(pub f32);
=
=#[derive(Bundle)]
@@ -53,12 +55,12 @@ pub struct PersonBundle {
=}
=
=impl PersonBundle {
- pub fn new(name: PersonName, sex: Sex, savings: f32) -> Self {
+ pub fn new(name: PersonName, sex: Sex, savings: Savings) -> Self {
= let display_name = name.to_string().into();
= Self {
= name,
= sex,
- savings: Savings(savings),
+ savings,
= marker: Person,
= display_name,
= }
@@ -79,12 +81,12 @@ impl Distribution<PersonBundle> for Standard {
= third: None,
= }
= };
- let savings = rng.gen_range(100.0..10000.0);
+ let savings = rng.gen_range(100.0..10000.0).into();
= PersonBundle::new(name, sex, savings)
= }
=}
=
-#[derive(Component, Reflect, Debug)]
+#[derive(Component, Reflect, Debug, Clone)]
=pub enum Sex {
= Male,
= Female,
@@ -109,7 +111,7 @@ impl Display for Sex {
= }
=}
=
-#[derive(Component, Reflect, Debug)]
+#[derive(Component, Reflect, Debug, Clone)]
=pub struct PersonName {
= first: String,
= second: String,
@@ -130,3 +132,66 @@ impl Display for PersonName {
= }
= }
=}
+
+// TODO: If the Snapshot trait experiment works, move it to the history module
+// The idea is to use the trait directly, by getting all types that
+// implement it and calling their methods. Also rename it to Snapshot.
+pub trait SnapshotTrait {
+ /// Given a reference to the world, produce and return a snapshot
+ ///
+ /// NOTE: A mutable reference to the world is required for technical
+ /// reasons, but please don't actually mutate the world while taking a
+ /// snapshot. Taking a snapshot should have no side effects.
+ fn capture(world: &mut World) -> Self;
+
+ /// Given a reference to the world, restore it's state to this captured in the snapshot
+ fn restore(&self, world: &mut World);
+}
+
+#[derive(Clone, Debug)]
+pub struct PopulationSnapshot {
+ pub persons: Vec<PersonDescription>,
+}
+
+impl SnapshotTrait for PopulationSnapshot {
+ fn capture(world: &mut World) -> Self {
+ let mut query = world.query_filtered::<(&PersonName, &Sex, &Savings), With<Person>>();
+ // TODO: Can I satisfy the borrow checker without collecting the iterator?
+ let persons = query
+ .iter(world)
+ .map(|(name, sex, savings)| PersonDescription {
+ name: name.clone(),
+ sex: sex.clone(),
+ savings: savings.clone(),
+ })
+ .collect_vec();
+
+ Self { persons }
+ }
+
+ fn restore(&self, world: &mut World) {
+ let mut query = world.query_filtered::<Entity, With<Person>>();
+ // TODO: Can I satisfy the borrow checker without collecting the iterator?
+ let persons = query.iter(world).collect_vec();
+ for person in persons {
+ world.despawn(person);
+ }
+
+ for person in self.persons.iter() {
+ world.spawn(PersonBundle::from(person.clone()));
+ }
+ }
+}
+
+impl From<PersonDescription> for PersonBundle {
+ fn from(value: PersonDescription) -> Self {
+ Self::new(value.name, value.sex, value.savings)
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct PersonDescription {
+ name: PersonName,
+ sex: Sex,
+ savings: Savings,
+}Add Macaco to the list of last names
On by
https://www.youtube.com/watch?v=xdMD6WDgqVk
index e7fbbe0..4a7a9ee 100644
--- a/src/names.rs
+++ b/src/names.rs
@@ -86,6 +86,7 @@ pub const LAST_NAMES: NamesList = &[
= "Longspur",
= "Loon",
= "Lynx",
+ "Macaco",
= "Macaw",
= "Magpie",
= "Mandrill",Move Snapshot trait to the history module
On by
Rename previous Snapshot struct to MainSnapshot to avoid a name collision. Also make all it's fields private, as there is no need for other modules to access it.
index fa0d5b7..0e3ef04 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -5,7 +5,6 @@ use crate::day_month::{DayMonth, Month};
=use crate::districts;
=use crate::districts::{DistrictsSystems, NewDistrictEstablished};
=use crate::population::PopulationSnapshot;
-use crate::population::SnapshotTrait;
=use crate::roads::{self, RoadsSystems};
=use crate::GameState;
=use bevy::ecs::system::SystemId;
@@ -36,7 +35,7 @@ impl Plugin for HistoryPlugin {
=#[derive(Resource, Default, Debug)]
=pub struct History {
= pub events: Vec<EventLogEntry>,
- pub snapshots: Vec<Snapshot>,
+ pub snapshots: Vec<MainSnapshot>,
=}
=
=/// A collection of events that will happen in the future from the time traveler's perspective
@@ -47,6 +46,21 @@ pub struct Future {
= events: Vec<EventLogEntry>,
=}
=
+// TODO: If the Snapshot trait experiment works, move it to the history module
+// The idea is to use the trait directly, by getting all types that
+// implement it and calling their methods. Also rename it to Snapshot.
+pub trait Snapshot {
+ /// Given a reference to the world, produce and return a snapshot
+ ///
+ /// NOTE: A mutable reference to the world is required for technical
+ /// reasons, but please don't actually mutate the world while taking a
+ /// snapshot. Taking a snapshot should have no side effects.
+ fn capture(world: &mut World) -> Self;
+
+ /// Given a reference to the world, restore it's state to this captured in the snapshot
+ fn restore(&self, world: &mut World);
+}
+
=/// A wrapper for any kind of event that should be replayed in explore mode
=#[derive(Clone, Debug)]
=pub enum HistoricalEvent {
@@ -108,12 +122,14 @@ fn register_historical_events(
=
=/// The main snapshot that binds them all
=#[derive(Clone, Debug)]
-pub struct Snapshot {
- pub date: DayMonth,
- pub buildings: buildings::Snapshot,
- pub districts: districts::Snapshot,
- pub roads: roads::Snapshot,
- pub population: PopulationSnapshot,
+pub struct MainSnapshot {
+ date: DayMonth,
+
+ // TODO: Replace all below with a single Vec<Box<dyn Snapshot>>,
+ buildings: buildings::Snapshot,
+ districts: districts::Snapshot,
+ roads: roads::Snapshot,
+ population: PopulationSnapshot,
=}
=
=// TODO: Find a way to coordinate with lay_initial_roads without exposing this system
@@ -144,7 +160,7 @@ pub fn take_snapshot(world: &mut World) {
=
= let timestamp: Timestamp = date.into();
=
- let snapshot = Snapshot {
+ let snapshot = MainSnapshot {
= date,
= buildings,
= districts,
@@ -159,7 +175,7 @@ pub fn take_snapshot(world: &mut World) {
= })
=}
=
-fn rollback(In(snapshot): In<Snapshot>, world: &mut World) {
+fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= // TODO: DRY on rollback. Maybe a macro?
= world.resource_scope(|_, mut game_state: Mut<NextState<GameState>>| {
= game_state.set(GameState::Explore);
@@ -200,7 +216,7 @@ fn rollback(In(snapshot): In<Snapshot>, world: &mut World) {
=
=#[derive(Debug, Resource)]
=pub struct HistorySystems {
- pub rollback: SystemId<Snapshot>,
+ pub rollback: SystemId<MainSnapshot>,
=}
=
=fn setup_history_systems(world: &mut World) {index e334e96..8180a0a 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,5 +1,6 @@
=use crate::date;
=use crate::date::SimulationFrameCounter;
+use crate::history::Snapshot;
=use crate::names::{FEMALE_NAMES, LAST_NAMES, MALE_NAMES};
=use crate::GameState;
=use bevy::prelude::*;
@@ -133,27 +134,13 @@ impl Display for PersonName {
= }
=}
=
-// TODO: If the Snapshot trait experiment works, move it to the history module
-// The idea is to use the trait directly, by getting all types that
-// implement it and calling their methods. Also rename it to Snapshot.
-pub trait SnapshotTrait {
- /// Given a reference to the world, produce and return a snapshot
- ///
- /// NOTE: A mutable reference to the world is required for technical
- /// reasons, but please don't actually mutate the world while taking a
- /// snapshot. Taking a snapshot should have no side effects.
- fn capture(world: &mut World) -> Self;
-
- /// Given a reference to the world, restore it's state to this captured in the snapshot
- fn restore(&self, world: &mut World);
-}
=
=#[derive(Clone, Debug)]
=pub struct PopulationSnapshot {
= pub persons: Vec<PersonDescription>,
=}
=
-impl SnapshotTrait for PopulationSnapshot {
+impl Snapshot for PopulationSnapshot {
= fn capture(world: &mut World) -> Self {
= let mut query = world.query_filtered::<(&PersonName, &Sex, &Savings), With<Person>>();
= // TODO: Can I satisfy the borrow checker without collecting the iterator?Refactor population
On by
-
Rename immigration system to plan_immigration in anticipation of events based migration system.
-
Implement standard distribution for PersonDescription instead of PersonBundle. It's more versatile this way, since converting from a description to a bundle is trivial.
-
Move code around more "high level" types on top for easier grasp of the "big picture".
index 8180a0a..19d602f 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -20,7 +20,7 @@ impl Plugin for PopulationPlugin {
= .register_type::<PersonName>()
= .add_systems(
= Update,
- immigration.run_if(
+ plan_immigration.run_if(
= in_state(GameState::Simulate)
= .and_then(resource_changed::<SimulationFrameCounter>)
= .and_then(date::past_hour(6.0)),
@@ -29,21 +29,42 @@ impl Plugin for PopulationPlugin {
= }
=}
=
-fn immigration(mut commands: Commands) {
+/// A logical description of a person suitable for events and snapshots
+#[derive(Clone, Debug)]
+pub struct PersonDescription {
+ name: PersonName,
+ sex: Sex,
+ savings: Savings,
+}
+
+impl Distribution<PersonDescription> for Standard {
+ fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> PersonDescription {
+ let sex = rng.gen::<Sex>();
+ let first_names = match sex {
+ Sex::Male => MALE_NAMES,
+ Sex::Female => FEMALE_NAMES,
+ };
+ let name = {
+ PersonName {
+ first: first_names.choose(rng).unwrap().to_string(),
+ second: LAST_NAMES.choose(rng).unwrap().to_string(),
+ third: None,
+ }
+ };
+ let savings = rng.gen_range(100.0..10000.0).into();
+ PersonDescription { name, savings, sex }
+ }
+}
+
+fn plan_immigration(mut commands: Commands) {
= let number = 20;
= info!("{number} people immigrated to Otterhide.");
=
= for _index in 1..=number {
- commands.spawn(random::<PersonBundle>());
+ commands.spawn(PersonBundle::from(random::<PersonDescription>()));
= }
=}
=
-#[derive(Component, Debug, Reflect)]
-pub struct Person;
-
-#[derive(Component, Reflect, Clone, Debug, From)]
-pub struct Savings(pub f32);
-
=#[derive(Bundle)]
=pub struct PersonBundle {
= pub name: PersonName,
@@ -68,25 +89,18 @@ impl PersonBundle {
= }
=}
=
-impl Distribution<PersonBundle> for Standard {
- fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> PersonBundle {
- let sex = rng.gen::<Sex>();
- let first_names = match sex {
- Sex::Male => MALE_NAMES,
- Sex::Female => FEMALE_NAMES,
- };
- let name = {
- PersonName {
- first: first_names.choose(rng).unwrap().to_string(),
- second: LAST_NAMES.choose(rng).unwrap().to_string(),
- third: None,
- }
- };
- let savings = rng.gen_range(100.0..10000.0).into();
- PersonBundle::new(name, sex, savings)
+impl From<PersonDescription> for PersonBundle {
+ fn from(value: PersonDescription) -> Self {
+ Self::new(value.name, value.sex, value.savings)
= }
=}
=
+#[derive(Component, Debug, Reflect)]
+pub struct Person;
+
+#[derive(Component, Reflect, Clone, Debug, From)]
+pub struct Savings(pub f32);
+
=#[derive(Component, Reflect, Debug, Clone)]
=pub enum Sex {
= Male,
@@ -169,16 +183,3 @@ impl Snapshot for PopulationSnapshot {
= }
= }
=}
-
-impl From<PersonDescription> for PersonBundle {
- fn from(value: PersonDescription) -> Self {
- Self::new(value.name, value.sex, value.savings)
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct PersonDescription {
- name: PersonName,
- sex: Sex,
- savings: Savings,
-}Split immigration system into plan and implement
On by
In preparation to capture and replay historical events.
index 19d602f..792620f 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -18,13 +18,18 @@ impl Plugin for PopulationPlugin {
= .register_type::<Savings>()
= .register_type::<Sex>()
= .register_type::<PersonName>()
+ .add_event::<Immigrated>()
= .add_systems(
= Update,
- plan_immigration.run_if(
+ plan_immigration.before(implement_immigration).run_if(
= in_state(GameState::Simulate)
= .and_then(resource_changed::<SimulationFrameCounter>)
= .and_then(date::past_hour(6.0)),
= ),
+ )
+ .add_systems(
+ Update,
+ implement_immigration.run_if(on_event::<Immigrated>()),
= );
= }
=}
@@ -56,12 +61,23 @@ impl Distribution<PersonDescription> for Standard {
= }
=}
=
-fn plan_immigration(mut commands: Commands) {
+#[derive(Event, Debug, Clone, From)]
+struct Immigrated(PersonDescription);
+
+fn plan_immigration(mut immigrated: EventWriter<Immigrated>) {
= let number = 20;
= info!("{number} people immigrated to Otterhide.");
=
= for _index in 1..=number {
- commands.spawn(PersonBundle::from(random::<PersonDescription>()));
+ let person = random::<PersonDescription>();
+ immigrated.send(Immigrated::from(person));
+ }
+}
+
+fn implement_immigration(mut immigrated: EventReader<Immigrated>, mut commands: Commands) {
+ for Immigrated(person) in immigrated.read().cloned() {
+ info!("{person:?} immigrated");
+ commands.spawn(PersonBundle::from(person));
= }
=}
=
@@ -148,7 +164,6 @@ impl Display for PersonName {
= }
=}
=
-
=#[derive(Clone, Debug)]
=pub struct PopulationSnapshot {
= pub persons: Vec<PersonDescription>,Implement immigrated events capturing and replay
On by
index 0e3ef04..a6516e7 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -4,7 +4,7 @@ use crate::date::{Date, DateSystems, NewMonth};
=use crate::day_month::{DayMonth, Month};
=use crate::districts;
=use crate::districts::{DistrictsSystems, NewDistrictEstablished};
-use crate::population::PopulationSnapshot;
+use crate::population::{Immigrated, PopulationSnapshot};
=use crate::roads::{self, RoadsSystems};
=use crate::GameState;
=use bevy::ecs::system::SystemId;
@@ -66,6 +66,7 @@ pub trait Snapshot {
=pub enum HistoricalEvent {
= ConstructionOrder(buildings::ConstructionOrder),
= NewDistrictEstablished(districts::NewDistrictEstablished),
+ Immigrated(Immigrated),
=}
=
=/// A historical event together with it's date
@@ -92,6 +93,9 @@ impl Display for HistoricalEvent {
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {
= write!(f, "New district established: {district:?}",)
= }
+ HistoricalEvent::Immigrated(Immigrated(person)) => {
+ write!(f, "A new immigrant arrived: {person:?}")
+ }
= }
= }
=}
@@ -101,6 +105,7 @@ fn register_historical_events(
= date: Res<Date>,
= mut construction_orders: EventReader<ConstructionOrder>,
= mut new_districts: EventReader<NewDistrictEstablished>,
+ mut immigrated: EventReader<Immigrated>,
=) {
= let date = date.0;
= for event in new_districts.read() {
@@ -113,6 +118,12 @@ fn register_historical_events(
= let event = HistoricalEvent::ConstructionOrder(event.to_owned());
= info!("Registering a historical event: {event}");
=
+ history.events.push(EventLogEntry { event, date });
+ }
+ for event in immigrated.read() {
+ let event = HistoricalEvent::Immigrated(event.to_owned());
+ info!("Registering a historical event: {event}");
+
= history.events.push(EventLogEntry { event, date });
= }
=}
@@ -229,6 +240,7 @@ fn replay_historical_events(
= mut future: ResMut<Future>,
= mut orders: EventWriter<ConstructionOrder>,
= mut districts: EventWriter<NewDistrictEstablished>,
+ mut immigrated: EventWriter<Immigrated>,
=) {
= future.events.retain(|entry| {
= if entry.date <= date.0 {
@@ -239,6 +251,9 @@ fn replay_historical_events(
= HistoricalEvent::NewDistrictEstablished(district) => {
= districts.send(district);
= }
+ HistoricalEvent::Immigrated(event) => {
+ immigrated.send(event);
+ }
= };
= false
= } else {index 25c8d8c..30ac4aa 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -1,6 +1,7 @@
=use crate::buildings::{BuildingDescription, ConstructionOrder};
=use crate::districts::NewDistrictEstablished;
=use crate::history::{HistoricalEvent, History};
+use crate::population::Immigrated;
=use bevy::math::Vec3;
=use std::fmt::Display;
=
@@ -66,6 +67,18 @@ impl Display for History {
= 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, ");")?;
+ }
= };
= }
=index 792620f..ae625cd 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -37,9 +37,9 @@ impl Plugin for PopulationPlugin {
=/// A logical description of a person suitable for events and snapshots
=#[derive(Clone, Debug)]
=pub struct PersonDescription {
- name: PersonName,
- sex: Sex,
- savings: Savings,
+ pub name: PersonName,
+ pub sex: Sex,
+ pub savings: Savings,
=}
=
=impl Distribution<PersonDescription> for Standard {
@@ -62,7 +62,7 @@ impl Distribution<PersonDescription> for Standard {
=}
=
=#[derive(Event, Debug, Clone, From)]
-struct Immigrated(PersonDescription);
+pub struct Immigrated(pub PersonDescription);
=
=fn plan_immigration(mut immigrated: EventWriter<Immigrated>) {
= let number = 20;Implement a "stable" person Id and register
On by
By stable I mean that it can be captured as a snapshot and later restored. It's important for inter-entity relations, like residency or family relations (mother, father, child).
index ae715c6..1c38020 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,6 +18,7 @@ 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;
@@ -50,6 +51,7 @@ fn main() {
= }),
= ..default()
= }))
+ .register_type::<Uuid>()
= .register_type::<SimulationParameters>()
= .init_resource::<SimulationParameters>()
= .init_resource::<PreloadedAssets>()index ae625cd..f90d825 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -3,7 +3,9 @@ use crate::date::SimulationFrameCounter;
=use crate::history::Snapshot;
=use crate::names::{FEMALE_NAMES, LAST_NAMES, MALE_NAMES};
=use crate::GameState;
+use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
+use bevy::utils::{HashMap, Uuid};
=use derive_more::From;
=use itertools::Itertools;
=use rand::distributions::Standard;
@@ -14,11 +16,15 @@ pub struct PopulationPlugin;
=
=impl Plugin for PopulationPlugin {
= fn build(&self, app: &mut App) {
- app.register_type::<Person>()
+ app.init_resource::<PersonsRegister>()
+ .register_type::<PersonsRegister>()
+ .register_type::<Person>()
+ .register_type::<PersonId>()
= .register_type::<Savings>()
= .register_type::<Sex>()
= .register_type::<PersonName>()
= .add_event::<Immigrated>()
+ .add_systems(Startup, register_population_systems)
= .add_systems(
= Update,
= plan_immigration.before(implement_immigration).run_if(
@@ -34,9 +40,14 @@ impl Plugin for PopulationPlugin {
= }
=}
=
+#[derive(Resource, Clone, Default, Debug, Reflect)]
+#[reflect(Resource)]
+pub struct PersonsRegister(pub HashMap<PersonId, Entity>);
+
=/// A logical description of a person suitable for events and snapshots
=#[derive(Clone, Debug)]
=pub struct PersonDescription {
+ pub id: PersonId,
= pub name: PersonName,
= pub sex: Sex,
= pub savings: Savings,
@@ -44,6 +55,7 @@ pub struct PersonDescription {
=
=impl Distribution<PersonDescription> for Standard {
= fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> PersonDescription {
+ let id = Uuid::from_bytes(rng.gen()).into();
= let sex = rng.gen::<Sex>();
= let first_names = match sex {
= Sex::Male => MALE_NAMES,
@@ -57,10 +69,36 @@ impl Distribution<PersonDescription> for Standard {
= }
= };
= let savings = rng.gen_range(100.0..10000.0).into();
- PersonDescription { name, savings, sex }
+ PersonDescription {
+ id,
+ name,
+ savings,
+ sex,
+ }
= }
=}
=
+#[derive(Resource, Debug)]
+pub struct PopulationSystems {
+ pub setup_new_person: SystemId<PersonDescription>,
+}
+
+fn register_population_systems(world: &mut World) {
+ let setup_new_person = world.register_system(setup_new_person);
+
+ world.insert_resource(PopulationSystems { setup_new_person });
+}
+
+pub fn setup_new_person(
+ In(person): In<PersonDescription>,
+ mut commands: Commands,
+ mut register: ResMut<PersonsRegister>,
+) {
+ let id = person.id.clone();
+ let entity = commands.spawn(PersonBundle::from(person)).id();
+ register.0.insert(id, entity);
+}
+
=#[derive(Event, Debug, Clone, From)]
=pub struct Immigrated(pub PersonDescription);
=
@@ -74,15 +112,20 @@ fn plan_immigration(mut immigrated: EventWriter<Immigrated>) {
= }
=}
=
-fn implement_immigration(mut immigrated: EventReader<Immigrated>, mut commands: Commands) {
+fn implement_immigration(
+ mut immigrated: EventReader<Immigrated>,
+ mut commands: Commands,
+ systems: Res<PopulationSystems>,
+) {
= for Immigrated(person) in immigrated.read().cloned() {
- info!("{person:?} immigrated");
- commands.spawn(PersonBundle::from(person));
+ info!("New immigrant {person:?} ");
+ commands.run_system_with_input(systems.setup_new_person, person);
= }
=}
=
=#[derive(Bundle)]
=pub struct PersonBundle {
+ pub id: PersonId,
= pub name: PersonName,
= pub sex: Sex,
= pub savings: Savings,
@@ -93,9 +136,10 @@ pub struct PersonBundle {
=}
=
=impl PersonBundle {
- pub fn new(name: PersonName, sex: Sex, savings: Savings) -> Self {
+ pub fn new(id: PersonId, name: PersonName, sex: Sex, savings: Savings) -> Self {
= let display_name = name.to_string().into();
= Self {
+ id,
= name,
= sex,
= savings,
@@ -107,13 +151,16 @@ impl PersonBundle {
=
=impl From<PersonDescription> for PersonBundle {
= fn from(value: PersonDescription) -> Self {
- Self::new(value.name, value.sex, value.savings)
+ Self::new(value.id, value.name, value.sex, value.savings)
= }
=}
=
=#[derive(Component, Debug, Reflect)]
=pub struct Person;
=
+#[derive(Component, Debug, Clone, Hash, PartialEq, Eq, Reflect, From)]
+pub struct PersonId(pub Uuid);
+
=#[derive(Component, Reflect, Clone, Debug, From)]
=pub struct Savings(pub f32);
=
@@ -171,11 +218,13 @@ pub struct PopulationSnapshot {
=
=impl Snapshot for PopulationSnapshot {
= fn capture(world: &mut World) -> Self {
- let mut query = world.query_filtered::<(&PersonName, &Sex, &Savings), With<Person>>();
+ let mut query =
+ world.query_filtered::<(&PersonId, &PersonName, &Sex, &Savings), With<Person>>();
= // TODO: Can I satisfy the borrow checker without collecting the iterator?
= let persons = query
= .iter(world)
- .map(|(name, sex, savings)| PersonDescription {
+ .map(|(id, name, sex, savings)| PersonDescription {
+ id: id.clone(),
= name: name.clone(),
= sex: sex.clone(),
= savings: savings.clone(),
@@ -193,8 +242,14 @@ impl Snapshot for PopulationSnapshot {
= world.despawn(person);
= }
=
+ world.resource_scope(|_, mut register: Mut<PersonsRegister>| register.0.clear());
+
= for person in self.persons.iter() {
- world.spawn(PersonBundle::from(person.clone()));
+ world.resource_scope(|world, systems: Mut<PopulationSystems>| {
+ world
+ .run_system_with_input(systems.setup_new_person, person.clone())
+ .unwrap()
+ });
= }
= }
=}index f4ad832..11948c8 100644
--- a/src/population_ui.rs
+++ b/src/population_ui.rs
@@ -1,4 +1,6 @@
=use crate::population::Person;
+use crate::population::PersonId;
+use crate::population::PersonsRegister;
=use crate::population::Savings;
=use crate::population::Sex;
=use bevy::prelude::*;
@@ -21,7 +23,11 @@ impl Plugin for PopulationUiPlugin {
=// 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<(&Name, &Sex, &Savings), With<Person>>) {
+fn paint_ui(
+ mut contexts: EguiContexts,
+ persons: Query<(&PersonId, &Name, &Sex, &Savings), With<Person>>,
+ register: Res<PersonsRegister>,
+) {
= egui::CentralPanel::default()
= .frame(Frame {
= inner_margin: Margin::symmetric(40., 20.),
@@ -32,17 +38,20 @@ fn paint_ui(mut contexts: EguiContexts, persons: Query<(&Name, &Sex, &Savings),
= ..default()
= })
= .show(contexts.ctx_mut(), |ui| {
- let count = persons.into_iter().count();
- ui.heading(format!("The {count} People of Otterhide"));
+ let count = persons.into_iter().len();
+ let registered = 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("Id");
= ui.label("Name");
= ui.label("Sex");
= ui.label("Savings");
= ui.end_row();
=
- for (name, sex, Savings(savings)) in persons.iter() {
+ for (id, name, sex, Savings(savings)) in persons.iter() {
+ ui.label(id.0.to_string());
= ui.label(name.to_string());
= ui.label(sex.to_string());
= ui.label(format!("{savings:.2} Œ"));Implement BuildingId component
On by
In preparation for BuildingRegister resource and residency relation between a Person and a Building.
index 49cfc77..82ef8f6 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -6,7 +6,9 @@ use crate::{GameState, PreloadedAssets};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
-use bevy::utils::HashMap;
+use bevy::utils::{HashMap, Uuid};
+use derive_more::Display;
+use derive_more::From;
=use itertools::Itertools;
=use rand::seq::IteratorRandom;
=use rand::{thread_rng, Rng};
@@ -22,6 +24,7 @@ impl Plugin for BuildingsPlugin {
= .init_resource::<BuildingScenes>()
= .register_type::<BuildingScenes>()
= .register_type::<BuildingClass>()
+ .register_type::<BuildingId>()
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_buildings_systems)
= .add_systems(OnEnter(GameState::Simulate), setup_building_scenes)
@@ -131,6 +134,7 @@ pub struct ConstructionOrder(pub BuildingDescription);
=
=#[derive(Clone, Debug)]
=pub struct BuildingDescription {
+ pub id: BuildingId,
= pub transform: Transform,
= pub variant: BuildingVariant,
= pub address: String,
@@ -142,6 +146,7 @@ pub struct Building;
=
=#[derive(Bundle)]
=pub struct BuildingBundle {
+ id: BuildingId,
= variant: BuildingVariant,
= scene: SceneBundle,
= name: Name,
@@ -150,16 +155,18 @@ pub struct BuildingBundle {
=}
=
=impl BuildingBundle {
- pub fn new(
- address: String,
- variant: BuildingVariant,
- transform: Transform,
- scenes: &BuildingScenes,
- ) -> Self {
+ pub fn new(description: BuildingDescription, scenes: &BuildingScenes) -> Self {
+ let BuildingDescription {
+ id,
+ address,
+ variant,
+ transform,
+ } = description;
= let scene = scenes.0.get(&variant).unwrap().clone();
= let name = address.into();
= let class = BuildingClass(variant.class.clone());
= Self {
+ id,
= variant,
= name,
= class,
@@ -173,6 +180,9 @@ impl BuildingBundle {
= }
=}
=
+#[derive(Component, Hash, PartialEq, Eq, Clone, Debug, From, Display, Reflect)]
+pub struct BuildingId(Uuid);
+
=/// A variant identifies the building scene
=///
=/// Variants come from the buildings.blend file (and buildings.glb exported from
@@ -256,6 +266,7 @@ fn order_construction(
= return;
= };
= let scenes = &scenes.get_class(&class.0);
+ let id = Uuid::new_v4().into();
= let variant = scenes.keys().choose(&mut rand::thread_rng()).unwrap();
= let district_name = districts.get(**parent).unwrap().to_string();
= let building_number = number.0.to_string();
@@ -267,6 +278,7 @@ fn order_construction(
=
= // Order the construction
= let description = BuildingDescription {
+ id,
= address,
= transform: transform.to_owned().into(),
= variant: variant.clone(),
@@ -292,25 +304,24 @@ fn construct_building(
= mut commands: Commands,
= scenes: Res<BuildingScenes>,
=) {
- let BuildingDescription {
- address,
- variant,
- transform,
- } = description.clone();
- let Vec3 { x, z, .. } = transform.translation;
-
- debug!("Spawning a new {variant} building at ({x:.2}, {z:.2})!");
- commands.spawn(BuildingBundle::new(address, variant, transform, &scenes));
+ let Vec3 { x, z, .. } = description.transform.translation;
+
+ debug!(
+ "Spawning a new {variant} building at ({x:.2}, {z:.2})!",
+ variant = &description.variant
+ );
+ commands.spawn(BuildingBundle::new(description, &scenes));
=}
=
=pub type Snapshot = Vec<BuildingDescription>;
=
=fn take_snapshot(
- buildings: Query<(&Name, &BuildingVariant, &Transform), With<Building>>,
+ buildings: Query<(&BuildingId, &Name, &BuildingVariant, &Transform), With<Building>>,
=) -> Snapshot {
= buildings
= .iter()
- .map(|(name, variant, transform)| BuildingDescription {
+ .map(|(id, name, variant, transform)| BuildingDescription {
+ id: id.clone(),
= address: name.to_string(),
= variant: variant.clone(),
= transform: *transform,index a6516e7..96bc753 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -80,6 +80,7 @@ impl Display for HistoricalEvent {
= fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
= match self {
= HistoricalEvent::ConstructionOrder(ConstructionOrder(BuildingDescription {
+ id,
= address,
= transform,
= variant,
@@ -87,7 +88,7 @@ impl Display for HistoricalEvent {
= let Vec3 { x, z, .. } = transform.translation;
= write!(
= f,
- "construction of a new building ({variant}) ordered at {address} ({x:.2}, {z:.2})",
+ "Construction of a new building ({variant}, {id}) ordered at {address} ({x:.2}, {z:.2})",
= )
= }
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {index 30ac4aa..c8261fa 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -34,6 +34,7 @@ impl Display for History {
=
= match event.event.clone() {
= HistoricalEvent::ConstructionOrder(ConstructionOrder(BuildingDescription {
+ id,
= address,
= transform,
= variant,
@@ -41,11 +42,13 @@ impl Display for History {
= let Vec3 { x, z, .. } = transform.translation;
=
= writeln!(f, "Insert into buildings (")?;
+ writeln!(f, " id,")?;
= writeln!(f, " date,")?;
= writeln!(f, " variant,")?;
= writeln!(f, " address,")?;
= writeln!(f, " coordinates")?;
= writeln!(f, ") values (")?;
+ writeln!(f, " {id}")?;
= writeln!(
= f,
= " {year}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"Implement BuildingsRegister resource
On by
It holds a map from a BuildingId to a Building Entity. It's intedned purpose is to enable the planned residency relation.
index 82ef8f6..022d4df 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -21,6 +21,8 @@ pub struct BuildingsPlugin;
=impl Plugin for BuildingsPlugin {
= fn build(&self, app: &mut App) {
= app.add_event::<ConstructionOrder>()
+ .init_resource::<BuildingsRegister>()
+ .register_type::<BuildingsRegister>()
= .init_resource::<BuildingScenes>()
= .register_type::<BuildingScenes>()
= .register_type::<BuildingClass>()
@@ -42,6 +44,10 @@ impl Plugin for BuildingsPlugin {
=#[derive(Resource)]
=struct BuildingAssets(Handle<Gltf>);
=
+#[derive(Resource, Clone, Default, Debug, Reflect)]
+#[reflect(Resource)]
+pub struct BuildingsRegister(pub HashMap<BuildingId, Entity>);
+
=#[derive(Resource, Debug)]
=pub struct BuildingsSystems {
= pub take_snapshot: SystemId<(), Snapshot>,
@@ -303,6 +309,7 @@ fn construct_building(
= In(description): In<BuildingDescription>,
= mut commands: Commands,
= scenes: Res<BuildingScenes>,
+ mut register: ResMut<BuildingsRegister>,
=) {
= let Vec3 { x, z, .. } = description.transform.translation;
=
@@ -310,7 +317,11 @@ fn construct_building(
= "Spawning a new {variant} building at ({x:.2}, {z:.2})!",
= variant = &description.variant
= );
- commands.spawn(BuildingBundle::new(description, &scenes));
+ let id = description.id.clone();
+ let entity = commands
+ .spawn(BuildingBundle::new(description, &scenes))
+ .id();
+ register.0.insert(id, entity);
=}
=
=pub type Snapshot = Vec<BuildingDescription>;Make the simulation 150 year long in production
On by
But keep it to 2 years in development.
index 1c38020..1f5f2b2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -111,7 +111,10 @@ impl Default for SimulationParameters {
= sun_gap: 200.,
= sun_elevation: 600.,
= first_year: 1865,
- years: 20,
+ #[cfg(debug_assertions)]
+ years: 2,
+ #[cfg(not(debug_assertions))]
+ years: 150,
= frame_offset: 2.0 * HOUR,
= frames_per_day: 6,
= }Implement residency relation
On by
The relation maps people to buildings and at the moment is implemented as a component on a Person entity. For now residency is randomly assigned to people without one, without regard to anything else.
Rolling back sometimes leads to runtime error. Something about entity not existing. Almost certainly a timing issue that will probably be resolved by migrating all snapshot capture / restore to synchronous logic (using the Snapshot trait).
index 022d4df..bf5caeb 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -189,6 +189,9 @@ impl BuildingBundle {
=#[derive(Component, Hash, PartialEq, Eq, Clone, Debug, From, Display, Reflect)]
=pub struct BuildingId(Uuid);
=
+// TODO: Keep track of houses with residents, empty, and full
+// pub struct Residents(Vec<PersonId>)
+
=/// A variant identifies the building scene
=///
=/// Variants come from the buildings.blend file (and buildings.glb exported fromindex 96bc753..7fdd608 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -4,7 +4,7 @@ use crate::date::{Date, DateSystems, NewMonth};
=use crate::day_month::{DayMonth, Month};
=use crate::districts;
=use crate::districts::{DistrictsSystems, NewDistrictEstablished};
-use crate::population::{Immigrated, PopulationSnapshot};
+use crate::population::{Immigrated, MovedIn, PopulationSnapshot};
=use crate::roads::{self, RoadsSystems};
=use crate::GameState;
=use bevy::ecs::system::SystemId;
@@ -67,6 +67,7 @@ pub enum HistoricalEvent {
= ConstructionOrder(buildings::ConstructionOrder),
= NewDistrictEstablished(districts::NewDistrictEstablished),
= Immigrated(Immigrated),
+ MovedIn(MovedIn),
=}
=
=/// A historical event together with it's date
@@ -97,6 +98,9 @@ impl Display for HistoricalEvent {
= HistoricalEvent::Immigrated(Immigrated(person)) => {
= write!(f, "A new immigrant arrived: {person:?}")
= }
+ HistoricalEvent::MovedIn(MovedIn { who, where_to }) => {
+ write!(f, "The person {who} moved into the building {where_to}")
+ }
= }
= }
=}
@@ -107,6 +111,7 @@ fn register_historical_events(
= mut construction_orders: EventReader<ConstructionOrder>,
= mut new_districts: EventReader<NewDistrictEstablished>,
= mut immigrated: EventReader<Immigrated>,
+ mut moved_in: EventReader<MovedIn>,
=) {
= let date = date.0;
= for event in new_districts.read() {
@@ -127,6 +132,11 @@ fn register_historical_events(
=
= history.events.push(EventLogEntry { event, date });
= }
+ for event in moved_in.read() {
+ let event = HistoricalEvent::MovedIn(event.to_owned());
+ info!("Registering a historical event: {event}");
+ history.events.push(EventLogEntry { event, date });
+ }
=}
=
=// TODO: Use hankjordan/bevy_save with a custom pipeline
@@ -242,6 +252,7 @@ fn replay_historical_events(
= mut orders: EventWriter<ConstructionOrder>,
= mut districts: EventWriter<NewDistrictEstablished>,
= mut immigrated: EventWriter<Immigrated>,
+ mut moved_in: EventWriter<MovedIn>,
=) {
= future.events.retain(|entry| {
= if entry.date <= date.0 {
@@ -255,6 +266,9 @@ fn replay_historical_events(
= HistoricalEvent::Immigrated(event) => {
= immigrated.send(event);
= }
+ HistoricalEvent::MovedIn(event) => {
+ moved_in.send(event);
+ }
= };
= false
= } else {index c8261fa..d0acfaa 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -1,7 +1,7 @@
=use crate::buildings::{BuildingDescription, ConstructionOrder};
=use crate::districts::NewDistrictEstablished;
=use crate::history::{HistoricalEvent, History};
-use crate::population::Immigrated;
+use crate::population::{Immigrated, MovedIn};
=use bevy::math::Vec3;
=use std::fmt::Display;
=
@@ -82,6 +82,12 @@ impl Display for History {
= 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});"
+ )?;
+ }
= };
= }
=index f90d825..672e9d1 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,3 +1,4 @@
+use crate::buildings::{self, BuildingId};
=use crate::date;
=use crate::date::SimulationFrameCounter;
=use crate::history::Snapshot;
@@ -6,7 +7,7 @@ use crate::GameState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy::utils::{HashMap, Uuid};
-use derive_more::From;
+use derive_more::{Display, From};
=use itertools::Itertools;
=use rand::distributions::Standard;
=use rand::prelude::*;
@@ -24,6 +25,7 @@ impl Plugin for PopulationPlugin {
= .register_type::<Sex>()
= .register_type::<PersonName>()
= .add_event::<Immigrated>()
+ .add_event::<MovedIn>()
= .add_systems(Startup, register_population_systems)
= .add_systems(
= Update,
@@ -36,7 +38,15 @@ impl Plugin for PopulationPlugin {
= .add_systems(
= Update,
= implement_immigration.run_if(on_event::<Immigrated>()),
- );
+ )
+ .add_systems(
+ Update,
+ search_for_houses.before(move_into_houses).run_if(
+ in_state(GameState::Simulate)
+ .and_then(resource_changed::<SimulationFrameCounter>),
+ ),
+ )
+ .add_systems(Update, move_into_houses.run_if(on_event::<MovedIn>()));
= }
=}
=
@@ -51,6 +61,7 @@ pub struct PersonDescription {
= pub name: PersonName,
= pub sex: Sex,
= pub savings: Savings,
+ pub residence: Option<BuildingId>,
=}
=
=impl Distribution<PersonDescription> for Standard {
@@ -74,6 +85,7 @@ impl Distribution<PersonDescription> for Standard {
= name,
= savings,
= sex,
+ residence: None,
= }
= }
=}
@@ -95,8 +107,12 @@ pub fn setup_new_person(
= mut register: ResMut<PersonsRegister>,
=) {
= let id = person.id.clone();
- let entity = commands.spawn(PersonBundle::from(person)).id();
- register.0.insert(id, entity);
+ let residence = person.residence.clone();
+ let mut entity = commands.spawn(PersonBundle::from(person));
+ if let Some(building_id) = residence {
+ entity.insert(Residence(building_id.clone()));
+ };
+ register.0.insert(id, entity.id());
=}
=
=#[derive(Event, Debug, Clone, From)]
@@ -123,6 +139,41 @@ fn implement_immigration(
= }
=}
=
+#[derive(Event, Debug, Clone)]
+pub struct MovedIn {
+ pub who: PersonId,
+ pub where_to: BuildingId,
+}
+
+// For now just assign random buildings for each homeless person, with a 50% chance
+fn search_for_houses(
+ persons: Query<&PersonId, Without<Residence>>,
+ buildings: Query<&BuildingId>,
+ mut moved_in: EventWriter<MovedIn>,
+) {
+ for person_id in persons.iter() {
+ if random() {
+ if let Some(building_id) = buildings.iter().choose(&mut thread_rng()) {
+ moved_in.send(MovedIn {
+ who: person_id.clone(),
+ where_to: building_id.clone(),
+ });
+ }
+ }
+ }
+}
+
+fn move_into_houses(
+ mut moved_in: EventReader<MovedIn>,
+ register: Res<PersonsRegister>,
+ mut commands: Commands,
+) {
+ for MovedIn { who, where_to } in moved_in.read() {
+ let entity = register.0.get(who).unwrap();
+ commands.entity(*entity).insert(Residence(where_to.clone()));
+ }
+}
+
=#[derive(Bundle)]
=pub struct PersonBundle {
= pub id: PersonId,
@@ -158,7 +209,7 @@ impl From<PersonDescription> for PersonBundle {
=#[derive(Component, Debug, Reflect)]
=pub struct Person;
=
-#[derive(Component, Debug, Clone, Hash, PartialEq, Eq, Reflect, From)]
+#[derive(Component, Debug, Clone, Hash, PartialEq, Eq, Reflect, From, Display)]
=pub struct PersonId(pub Uuid);
=
=#[derive(Component, Reflect, Clone, Debug, From)]
@@ -211,6 +262,9 @@ impl Display for PersonName {
= }
=}
=
+#[derive(Component, Debug, Clone)]
+pub struct Residence(pub BuildingId);
+
=#[derive(Clone, Debug)]
=pub struct PopulationSnapshot {
= pub persons: Vec<PersonDescription>,
@@ -219,15 +273,16 @@ pub struct PopulationSnapshot {
=impl Snapshot for PopulationSnapshot {
= fn capture(world: &mut World) -> Self {
= let mut query =
- world.query_filtered::<(&PersonId, &PersonName, &Sex, &Savings), With<Person>>();
+ world.query_filtered::<(&PersonId, &PersonName, &Sex, &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)| PersonDescription {
+ .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()),
= })
= .collect_vec();
=index 11948c8..2f5a177 100644
--- a/src/population_ui.rs
+++ b/src/population_ui.rs
@@ -1,6 +1,9 @@
+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::*;
@@ -25,8 +28,10 @@ impl Plugin for PopulationUiPlugin {
=
=fn paint_ui(
= mut contexts: EguiContexts,
- persons: Query<(&PersonId, &Name, &Sex, &Savings), With<Person>>,
- register: Res<PersonsRegister>,
+ 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 {
@@ -39,22 +44,29 @@ fn paint_ui(
= })
= .show(contexts.ctx_mut(), |ui| {
= let count = persons.into_iter().len();
- let registered = register.0.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("Id");
= ui.label("Name");
= ui.label("Sex");
= ui.label("Savings");
+ ui.label("Address");
+ ui.label("Id");
= ui.end_row();
=
- for (id, name, sex, Savings(savings)) in persons.iter() {
- ui.label(id.0.to_string());
+ for (id, name, sex, Savings(savings), residence) in persons.iter() {
+ let address = residence
+ .map(|Residence(id)| buildings_register.0.get(id).unwrap())
+ .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();
= }
= })Implement the capacity and overcrowded components
On by
For buildings. The initial capacity is always 2, and it is not used or updated anywhere yet. The intention is to update it when people move in and out of houses. The design with two different types is chosen for performance and ease of querying for empty houses and houses with too many people.
index bf5caeb..0f92dea 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -14,6 +14,7 @@ use rand::seq::IteratorRandom;
=use rand::{thread_rng, Rng};
=use regex::Regex;
=use std::fmt::Display;
+use std::ops::Neg;
=use std::str::FromStr;
=
=pub struct BuildingsPlugin;
@@ -22,6 +23,8 @@ impl Plugin for BuildingsPlugin {
= fn build(&self, app: &mut App) {
= app.add_event::<ConstructionOrder>()
= .init_resource::<BuildingsRegister>()
+ .register_type::<HousingCapacity>()
+ .register_type::<Overcrowded>()
= .register_type::<BuildingsRegister>()
= .init_resource::<BuildingScenes>()
= .register_type::<BuildingScenes>()
@@ -144,6 +147,7 @@ pub struct BuildingDescription {
= pub transform: Transform,
= pub variant: BuildingVariant,
= pub address: String,
+ pub capacity: i16,
=}
=
=/// A tag for building entities
@@ -167,6 +171,7 @@ impl BuildingBundle {
= address,
= variant,
= transform,
+ ..
= } = description;
= let scene = scenes.0.get(&variant).unwrap().clone();
= let name = address.into();
@@ -192,6 +197,14 @@ pub struct BuildingId(Uuid);
=// TODO: Keep track of houses with residents, empty, and full
=// pub struct Residents(Vec<PersonId>)
=
+/// How many more people can this building house?
+#[derive(Component, Debug, Clone, Reflect)]
+pub struct HousingCapacity(u8);
+
+/// A negative capacity
+#[derive(Component, Debug, Clone, Reflect)]
+pub struct Overcrowded(u8);
+
=/// A variant identifies the building scene
=///
=/// Variants come from the buildings.blend file (and buildings.glb exported from
@@ -291,6 +304,7 @@ fn order_construction(
= address,
= transform: transform.to_owned().into(),
= variant: variant.clone(),
+ capacity: 2,
= };
= build_events.send(ConstructionOrder(description));
= }
@@ -320,25 +334,53 @@ fn construct_building(
= "Spawning a new {variant} building at ({x:.2}, {z:.2})!",
= variant = &description.variant
= );
+
= let id = description.id.clone();
- let entity = commands
- .spawn(BuildingBundle::new(description, &scenes))
- .id();
- register.0.insert(id, entity);
+ let capacity = description.capacity;
+ let mut entity = commands.spawn(BuildingBundle::new(description, &scenes));
+
+ if capacity > 0 {
+ entity.insert(HousingCapacity(capacity as u8));
+ } else if capacity < 0 {
+ entity.insert(Overcrowded(capacity.saturating_abs() as u8));
+ };
+
+ register.0.insert(id, entity.id());
=}
=
=pub type Snapshot = Vec<BuildingDescription>;
=
=fn take_snapshot(
- buildings: Query<(&BuildingId, &Name, &BuildingVariant, &Transform), With<Building>>,
+ buildings: Query<
+ (
+ &BuildingId,
+ &Name,
+ &BuildingVariant,
+ &Transform,
+ Option<&HousingCapacity>,
+ Option<&Overcrowded>,
+ ),
+ With<Building>,
+ >,
=) -> Snapshot {
= buildings
= .iter()
- .map(|(id, name, variant, transform)| BuildingDescription {
- id: id.clone(),
- address: name.to_string(),
- variant: variant.clone(),
- transform: *transform,
+ .map(|(id, name, variant, transform, capacity, overcrowded)| {
+ let capacity = match (capacity, overcrowded) {
+ (None, None) => 0,
+ (None, Some(overcrowded)) => (overcrowded.0 as i16).neg(),
+ (Some(capacity), None) => capacity.0 as i16,
+ (Some(_), Some(_)) => {
+ panic!("A house at {name} have capacity and is overcrowded at the same time!")
+ }
+ };
+ BuildingDescription {
+ id: id.clone(),
+ address: name.to_string(),
+ variant: variant.clone(),
+ transform: *transform,
+ capacity,
+ }
= })
= .collect()
=}index 7fdd608..681ce4a 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -85,11 +85,12 @@ impl Display for HistoricalEvent {
= address,
= transform,
= variant,
+ capacity,
= })) => {
= let Vec3 { x, z, .. } = transform.translation;
= write!(
= f,
- "Construction of a new building ({variant}, {id}) ordered at {address} ({x:.2}, {z:.2})",
+ "Construction of a new building ({variant}, {id}) with a capacity of {capacity} ordered at {address} ({x:.2}, {z:.2})",
= )
= }
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {index d0acfaa..0c24f7e 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -38,6 +38,7 @@ impl Display for History {
= address,
= transform,
= variant,
+ capacity,
= })) => {
= let Vec3 { x, z, .. } = transform.translation;
=
@@ -47,6 +48,7 @@ impl Display for History {
= writeln!(f, " variant,")?;
= writeln!(f, " address,")?;
= writeln!(f, " coordinates")?;
+ writeln!(f, " capacity")?;
= writeln!(f, ") values (")?;
= writeln!(f, " {id}")?;
= writeln!(
@@ -56,6 +58,7 @@ impl Display for History {
= writeln!(f, " {variant}")?;
= writeln!(f, " {address}")?;
= writeln!(f, " ({x:.3}, {z:.3})")?;
+ writeln!(f, " {capacity}")?;
= writeln!(f, ");")?;
= }
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {index 672e9d1..2f6a1ea 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,4 +1,4 @@
-use crate::buildings::{self, BuildingId};
+use crate::buildings::BuildingId;
=use crate::date;
=use crate::date::SimulationFrameCounter;
=use crate::history::Snapshot;index 2f5a177..2b9f87a 100644
--- a/src/population_ui.rs
+++ b/src/population_ui.rs
@@ -29,7 +29,7 @@ impl Plugin for PopulationUiPlugin {
=fn paint_ui(
= mut contexts: EguiContexts,
= persons: Query<(&PersonId, &Name, &Sex, &Savings, Option<&Residence>), With<Person>>,
- buildings: Query<(&Name), With<Building>>,
+ buildings: Query<&Name, With<Building>>,
= persons_register: Res<PersonsRegister>,
= buildings_register: Res<BuildingsRegister>,
=) {Keep track of housing capacity
On by
We now have an overcrowded component that we are not using yet. We have to think on how to handle an overcrowded house.
index 0f92dea..7c953f2 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -199,11 +199,11 @@ pub struct BuildingId(Uuid);
=
=/// How many more people can this building house?
=#[derive(Component, Debug, Clone, Reflect)]
-pub struct HousingCapacity(u8);
+pub struct HousingCapacity(pub u8);
=
=/// A negative capacity
=#[derive(Component, Debug, Clone, Reflect)]
-pub struct Overcrowded(u8);
+pub struct Overcrowded(pub u8);
=
=/// A variant identifies the building scene
=///index 2f6a1ea..9834d04 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,4 +1,4 @@
-use crate::buildings::BuildingId;
+use crate::buildings::{BuildingId, BuildingsRegister, HousingCapacity, Overcrowded};
=use crate::date;
=use crate::date::SimulationFrameCounter;
=use crate::history::Snapshot;
@@ -148,12 +148,12 @@ pub struct MovedIn {
=// For now just assign random buildings for each homeless person, with a 50% chance
=fn search_for_houses(
= persons: Query<&PersonId, Without<Residence>>,
- buildings: Query<&BuildingId>,
+ buildings: Query<(&BuildingId, &HousingCapacity)>,
= mut moved_in: EventWriter<MovedIn>,
=) {
= for person_id in persons.iter() {
= if random() {
- if let Some(building_id) = buildings.iter().choose(&mut thread_rng()) {
+ if let Some((building_id, capacity)) = buildings.iter().choose(&mut thread_rng()) {
= moved_in.send(MovedIn {
= who: person_id.clone(),
= where_to: building_id.clone(),
@@ -165,12 +165,23 @@ fn search_for_houses(
=
=fn move_into_houses(
= mut moved_in: EventReader<MovedIn>,
- register: Res<PersonsRegister>,
+ mut buildings: Query<&mut HousingCapacity>,
+ buildings_register: Res<BuildingsRegister>,
+ persons_register: Res<PersonsRegister>,
= mut commands: Commands,
=) {
= for MovedIn { who, where_to } in moved_in.read() {
- let entity = register.0.get(who).unwrap();
- commands.entity(*entity).insert(Residence(where_to.clone()));
+ let person = persons_register.0.get(who).unwrap();
+ let building = buildings_register.0.get(where_to).unwrap();
+ let mut capacity = buildings.get_mut(*building).unwrap();
+
+ commands.entity(*person).insert(Residence(where_to.clone()));
+
+ // TODO: Consider overcrowding
+ capacity.0 = capacity.0.saturating_sub(1);
+ if capacity.0 == 0 {
+ commands.entity(*building).remove::<HousingCapacity>();
+ }
= }
=}
=Use Snapshot trait for buildings
On by
Also fix the problem where BuildingsRegister was not cleared on restore.
index 7c953f2..2f0e434 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -1,5 +1,6 @@
=use crate::date::SimulationFrameCounter;
=use crate::districts::District;
+use crate::history::Snapshot;
=use crate::parcels::{Parcel, ParcelNumber};
=use crate::population::Person;
=use crate::{GameState, PreloadedAssets};
@@ -53,21 +54,13 @@ pub struct BuildingsRegister(pub HashMap<BuildingId, Entity>);
=
=#[derive(Resource, Debug)]
=pub struct BuildingsSystems {
- pub take_snapshot: SystemId<(), Snapshot>,
- pub rollback: SystemId<Snapshot>,
= pub construct_building: SystemId<BuildingDescription>,
=}
=
=fn register_buildings_systems(world: &mut World) {
- let take_snapshot = world.register_system(take_snapshot);
- let rollback = world.register_system(rollback);
= let construct_building = world.register_system(construct_building);
=
- world.insert_resource(BuildingsSystems {
- take_snapshot,
- rollback,
- construct_building,
- });
+ world.insert_resource(BuildingsSystems { construct_building });
=}
=
=fn setup_assets(
@@ -348,54 +341,55 @@ fn construct_building(
= register.0.insert(id, entity.id());
=}
=
-pub type Snapshot = Vec<BuildingDescription>;
-
-fn take_snapshot(
- buildings: Query<
- (
- &BuildingId,
- &Name,
- &BuildingVariant,
- &Transform,
- Option<&HousingCapacity>,
- Option<&Overcrowded>,
- ),
- With<Building>,
- >,
-) -> Snapshot {
- buildings
- .iter()
- .map(|(id, name, variant, transform, capacity, overcrowded)| {
- let capacity = match (capacity, overcrowded) {
- (None, None) => 0,
- (None, Some(overcrowded)) => (overcrowded.0 as i16).neg(),
- (Some(capacity), None) => capacity.0 as i16,
- (Some(_), Some(_)) => {
- panic!("A house at {name} have capacity and is overcrowded at the same time!")
+pub type BuildingsSnapshot = Vec<BuildingDescription>;
+impl Snapshot for BuildingsSnapshot {
+ fn capture(world: &mut World) -> Self {
+ world
+ .query_filtered::<(
+ &BuildingId,
+ &Name,
+ &BuildingVariant,
+ &Transform,
+ Option<&HousingCapacity>,
+ Option<&Overcrowded>,
+ ), With<Building>>()
+ .iter(&world)
+ .map(|(id, name, variant, transform, capacity, overcrowded)| {
+ let capacity = match (capacity, overcrowded) {
+ (None, None) => 0,
+ (None, Some(overcrowded)) => (overcrowded.0 as i16).neg(),
+ (Some(capacity), None) => capacity.0 as i16,
+ (Some(_), Some(_)) => {
+ panic!(
+ "A house at {name} have capacity and is overcrowded at the same time!"
+ )
+ }
+ };
+ BuildingDescription {
+ id: id.clone(),
+ address: name.to_string(),
+ variant: variant.clone(),
+ transform: *transform,
+ capacity,
= }
- };
- BuildingDescription {
- id: id.clone(),
- address: name.to_string(),
- variant: variant.clone(),
- transform: *transform,
- capacity,
- }
- })
- .collect()
-}
-
-fn rollback(
- In(snapshot): In<Snapshot>,
- buildings: Query<Entity, With<Building>>,
- mut commands: Commands,
- systems: Res<BuildingsSystems>,
-) {
- for entity in buildings.iter() {
- commands.entity(entity).despawn_recursive();
+ })
+ .collect()
= }
=
- for description in snapshot {
- commands.run_system_with_input(systems.construct_building, description.to_owned());
+ fn restore(&self, world: &mut World) {
+ world.resource_scope(|world, mut register: Mut<BuildingsRegister>| {
+ for entity in register.0.values() {
+ despawn_with_children_recursive(world, *entity)
+ }
+ register.0.clear();
+ });
+
+ world.resource_scope(|world, systems: Mut<BuildingsSystems>| {
+ for description in self {
+ world
+ .run_system_with_input(systems.construct_building, description.to_owned())
+ .unwrap();
+ }
+ });
= }
=}index 681ce4a..b8b80c8 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,5 +1,5 @@
-use crate::buildings::{self, BuildingDescription};
-use crate::buildings::{BuildingsSystems, ConstructionOrder};
+use crate::buildings::ConstructionOrder;
+use crate::buildings::{self, BuildingDescription, BuildingsSnapshot};
=use crate::date::{Date, DateSystems, NewMonth};
=use crate::day_month::{DayMonth, Month};
=use crate::districts;
@@ -149,7 +149,7 @@ pub struct MainSnapshot {
= date: DayMonth,
=
= // TODO: Replace all below with a single Vec<Box<dyn Snapshot>>,
- buildings: buildings::Snapshot,
+ buildings: BuildingsSnapshot,
= districts: districts::Snapshot,
= roads: roads::Snapshot,
= population: PopulationSnapshot,
@@ -173,12 +173,7 @@ pub fn take_snapshot(world: &mut World) {
= world.resource_scope(|_, systems: Mut<DistrictsSystems>| systems.take_snapshot);
= world.run_system(system).unwrap()
= };
- let buildings = {
- let system =
- world.resource_scope(|_, systems: Mut<BuildingsSystems>| systems.take_snapshot);
- world.run_system(system).unwrap()
- };
-
+ let buildings = BuildingsSnapshot::capture(world);
= let population = PopulationSnapshot::capture(world);
=
= let timestamp: Timestamp = date.into();
@@ -218,12 +213,7 @@ fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= .run_system_with_input(system, snapshot.districts)
= .unwrap();
= }
- {
- let system = world.resource_scope(|_, systems: Mut<BuildingsSystems>| systems.rollback);
- world
- .run_system_with_input(system, snapshot.buildings)
- .unwrap();
- }
+ snapshot.buildings.restore(world);
= snapshot.population.restore(world);
=
= let events = world.resource_scope(|_, history: Mut<History>| history.events.clone());Create 2 new districts with paths
On by
The paths use modifiers to excavate the district ground and prevent z-fighting (clipping?).
index 277bc87..d857def 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex 10b22bd..24eb68e 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differAdded district of 300x540 and added roads and fixed buildings of 2 older districts
On by
index d857def..fcff5fc 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex 24eb68e..3eff3fe 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differAdded lake, added trees and fountains, added 2 big houses
On by
index 53136f0..5771c7e 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differindex fcff5fc..d5426f0 100644
Binary files a/art/districts.blend and b/art/districts.blend differnew file mode 100644
index 0000000..47aec79
Binary files /dev/null and b/art/models/fountainRoundDetail.glb differnew file mode 100644
index 0000000..fcb1c75
Binary files /dev/null and b/art/models/treeHighRound.glb differnew file mode 100644
index 0000000..6190c72
Binary files /dev/null and b/art/models/treeSmall.glb differindex 4f7b2fd..557ca45 100644
Binary files a/assets/buildings.glb and b/assets/buildings.glb differindex 3eff3fe..6545fa2 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differAdded second lake and extra trees.
On by
index d5426f0..66c1f26 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex 6545fa2..2b013a7 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differAdded 180x180 tile, added hill and lake a bit deeper. Fixed small bug.
On by
index 66c1f26..c1315db 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex 2b013a7..17bedf9 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differindex 1f5f2b2..b5525d6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -112,7 +112,7 @@ impl Default for SimulationParameters {
= sun_elevation: 600.,
= first_year: 1865,
= #[cfg(debug_assertions)]
- years: 2,
+ years: 20,
= #[cfg(not(debug_assertions))]
= years: 150,
= frame_offset: 2.0 * HOUR,Use Snapshot trait for roads
On by
index b8b80c8..d72ef71 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,11 +1,10 @@
-use crate::buildings::ConstructionOrder;
-use crate::buildings::{self, BuildingDescription, BuildingsSnapshot};
+use crate::buildings::{self, BuildingDescription, BuildingsSnapshot, ConstructionOrder};
=use crate::date::{Date, DateSystems, NewMonth};
=use crate::day_month::{DayMonth, Month};
=use crate::districts;
=use crate::districts::{DistrictsSystems, NewDistrictEstablished};
=use crate::population::{Immigrated, MovedIn, PopulationSnapshot};
-use crate::roads::{self, RoadsSystems};
+use crate::roads::RoadsSnapshot;
=use crate::GameState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
@@ -151,7 +150,7 @@ pub struct MainSnapshot {
= // TODO: Replace all below with a single Vec<Box<dyn Snapshot>>,
= buildings: BuildingsSnapshot,
= districts: districts::Snapshot,
- roads: roads::Snapshot,
+ roads: RoadsSnapshot,
= population: PopulationSnapshot,
=}
=
@@ -163,11 +162,7 @@ pub fn take_snapshot(world: &mut World) {
= world.run_system(system).unwrap()
= };
=
- let roads = {
- let system = world.resource_scope(|_, systems: Mut<RoadsSystems>| systems.take_snapshot);
- world.run_system(system).unwrap()
- };
-
+ let roads = RoadsSnapshot::capture(world);
= let districts = {
= let system =
= world.resource_scope(|_, systems: Mut<DistrictsSystems>| systems.take_snapshot);
@@ -203,10 +198,7 @@ fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= let system = world.resource_scope(|_, systems: Mut<DateSystems>| systems.rollback);
= world.run_system_with_input(system, snapshot.date).unwrap();
= }
- {
- let system = world.resource_scope(|_, systems: Mut<RoadsSystems>| systems.rollback);
- world.run_system_with_input(system, snapshot.roads).unwrap();
- }
+ snapshot.roads.restore(world);
= {
= let system = world.resource_scope(|_, systems: Mut<DistrictsSystems>| systems.rollback);
= worldindex 18b77db..ebb5132 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,4 +1,6 @@
=use crate::coordinates::{Coordinates, Direction};
+
+use crate::history::Snapshot;
=use crate::{history, GameState, PreloadedAssets, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
@@ -68,21 +70,15 @@ fn lay_initial_roads(
=/// A collection of one-shot systems that create new roads
=#[derive(Resource, Debug)]
=pub struct RoadsSystems {
- pub take_snapshot: SystemId<(), Snapshot>,
- pub rollback: SystemId<Snapshot>,
= pub construct_road_section: SystemId<(Coordinates, RoadSection)>,
= pub implement_road_plan: SystemId<RoadPlan>,
=}
=
=fn register_road_systems(world: &mut World) {
- let take_snapshot = world.register_system(take_snapshot);
- let rollback = world.register_system(rollback);
= let construct_road_section = world.register_system(construct_road_section);
= let implement_road_plan = world.register_system(implement_road_plan);
=
= world.insert_resource(RoadsSystems {
- take_snapshot,
- rollback,
= construct_road_section,
= implement_road_plan,
= });
@@ -138,44 +134,48 @@ fn construct_road_section(
= };
=}
=
-fn implement_road_plan(
- In(plan): In<RoadPlan>,
- mut commands: Commands,
- constructors: Res<RoadsSystems>,
-) {
+fn implement_road_plan(In(plan): In<RoadPlan>, mut commands: Commands, systems: Res<RoadsSystems>) {
= for section in plan.sections {
- commands.run_system_with_input(constructors.construct_road_section, section)
+ commands.run_system_with_input(systems.construct_road_section, section)
= }
=}
=
=// TODO: Consider wrapping the code below in roads::snapshots module
=
-pub type Snapshot = RoadPlan;
+pub type RoadsSnapshot = RoadPlan;
=
-pub fn take_snapshot(roads: Res<Roads>, sections: Query<&RoadSection>) -> Snapshot {
- // let roads = world.resource::<Roads>();
- let mut plan = RoadPlan::default();
+impl Snapshot for RoadsSnapshot {
+ fn capture(world: &mut World) -> Self {
+ // let roads = world.resource::<Roads>();
+ let mut plan = RoadPlan::default();
=
- for (coordinates, entity) in roads.sections.clone() {
- let section = sections.get(entity).unwrap();
- plan.sections.insert((coordinates, *section));
- }
+ let mut sections = world.query::<&RoadSection>();
+ world.resource_scope(|world, roads: Mut<Roads>| {
+ for (coordinates, entity) in roads.sections.clone() {
+ let section = sections.get(world, entity).unwrap();
+ plan.sections.insert((coordinates, *section));
+ }
+ });
=
- plan
-}
+ plan
+ }
=
-fn rollback(
- In(snapshot): In<Snapshot>,
- sections: Query<Entity, With<RoadSection>>,
- mut roads: ResMut<Roads>,
- mut commands: Commands,
- systems: Res<RoadsSystems>,
-) {
- for entity in sections.iter() {
- commands.entity(entity).despawn_recursive();
+ fn restore(&self, world: &mut World) {
+ world.resource_scope(|world, mut roads: Mut<Roads>| {
+ for entity in roads.sections.values() {
+ despawn_with_children_recursive(world, *entity);
+ }
+ roads.sections.clear();
+ });
+
+ let RoadsSystems {
+ implement_road_plan,
+ ..
+ } = world.resource();
+ world
+ .run_system_with_input(*implement_road_plan, self.clone())
+ .unwrap();
= }
- roads.sections.clear();
- commands.run_system_with_input(systems.implement_road_plan, snapshot);
=}
=
=/// Represents road sections with their positions, but without reference to entities in the world.Re-work the capacity and overcrowding logic
On by
Having two mutually exclusive and mutable components (Capacity and Overcrowded) proved to be a potent footgun. Now HousingCapacity is a "constant" for a building (technically it can be changed, but it should not). It denotes the total capacity that doesn't depend on how many residents are actually in the house. Kind of like a floor space. For filtering there are two markers: IsOvercrowded and HasSpareCapacity. They are mutually exclusive, but since they are effectively boolean (each one is either assigned to an entity or not), it's much easier to manage them.
The residency relation between buildings and persons is now double
linked. In addition to Residency(BuildingId) on a person that we already
had, now there is Residents(Vec
While working on this I wrote some nasty bugs. In an attempt to remove them I revamped the logging. In general I think it's a very good improvement. I re-discovered that values with Debug trait can always be pretty-printed using the :#? modifier. So instead of writing prose with deconstructed values now I'm just pretty-printing big data structures. This yields much more detailed and useful (although also much longer) logs.
Some of the bugs remain. Specifically sometimes (not sure when) the program crashes in playback on person moving in. It's always either a BuildingId or PersonId not present in a respective register. It doesn't seem deterministic. Sometimes I can playback the simulation few times without an error, and then BAM!
Currently after a crash there is no way to experiment on the same simulation, which makes debugging harder. So now I want to implement a save - load feature (that we need for the final product anyway), and see if there is some more specific pattern to the errors.
index 2f0e434..8d2a09f 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -2,12 +2,12 @@ use crate::date::SimulationFrameCounter;
=use crate::districts::District;
=use crate::history::Snapshot;
=use crate::parcels::{Parcel, ParcelNumber};
-use crate::population::Person;
+use crate::population::{MovedIn, Person, PersonId};
=use crate::{GameState, PreloadedAssets};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
-use bevy::utils::{HashMap, Uuid};
+use bevy::utils::{HashMap, HashSet, Uuid};
=use derive_more::Display;
=use derive_more::From;
=use itertools::Itertools;
@@ -15,7 +15,6 @@ use rand::seq::IteratorRandom;
=use rand::{thread_rng, Rng};
=use regex::Regex;
=use std::fmt::Display;
-use std::ops::Neg;
=use std::str::FromStr;
=
=pub struct BuildingsPlugin;
@@ -25,7 +24,8 @@ impl Plugin for BuildingsPlugin {
= app.add_event::<ConstructionOrder>()
= .init_resource::<BuildingsRegister>()
= .register_type::<HousingCapacity>()
- .register_type::<Overcrowded>()
+ .register_type::<IsOvercrowded>()
+ .register_type::<HasSpareCapacity>()
= .register_type::<BuildingsRegister>()
= .init_resource::<BuildingScenes>()
= .register_type::<BuildingScenes>()
@@ -41,10 +41,21 @@ impl Plugin for BuildingsPlugin {
= .and_then(resource_changed::<SimulationFrameCounter>),
= ),
= )
+ .add_systems(Update, move_into_houses.run_if(on_event::<MovedIn>()))
= .add_systems(Update, receive_orders);
= }
=}
=
+fn move_into_houses(
+ mut moved_in: EventReader<MovedIn>,
+ systems: Res<BuildingsSystems>,
+ mut commands: Commands,
+) {
+ for event in moved_in.read() {
+ commands.run_system_with_input(systems.register_resident, event.clone())
+ }
+}
+
=#[derive(Resource)]
=struct BuildingAssets(Handle<Gltf>);
=
@@ -55,12 +66,50 @@ pub struct BuildingsRegister(pub HashMap<BuildingId, Entity>);
=#[derive(Resource, Debug)]
=pub struct BuildingsSystems {
= pub construct_building: SystemId<BuildingDescription>,
+ pub register_resident: SystemId<MovedIn>,
=}
=
=fn register_buildings_systems(world: &mut World) {
= let construct_building = world.register_system(construct_building);
+ let register_resident = world.register_system(register_resident);
+
+ world.insert_resource(BuildingsSystems {
+ construct_building,
+ register_resident,
+ });
+}
=
- world.insert_resource(BuildingsSystems { construct_building });
+fn register_resident(
+ In(moved_in): In<MovedIn>,
+ mut buildings: Query<(&HousingCapacity, Option<&mut Residents>)>,
+ buildings_register: ResMut<BuildingsRegister>,
+ mut commands: Commands,
+) {
+ info!("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:#?}",
+ id = moved_in.where_to
+ );
+ };
+
+ let (capacity, residents) = buildings.get_mut(*building).unwrap();
+ match residents {
+ None => {
+ let residents = Residents(HashSet::from_iter(vec![moved_in.who.clone()]));
+ commands.entity(*building).insert(residents);
+ // NOTE: We assume that empty building has capacity > 1, so no need to mark it with IsOvercrowded or remove HasSpareCapacity. Wrong?
+ }
+ Some(mut residents) => {
+ residents.0.insert(moved_in.who.clone());
+ if residents.0.len() >= capacity.0 as usize {
+ commands.entity(*building).remove::<HasSpareCapacity>();
+ }
+ if residents.0.len() > capacity.0 as usize {
+ commands.entity(*building).insert(IsOvercrowded);
+ }
+ }
+ }
=}
=
=fn setup_assets(
@@ -140,7 +189,8 @@ pub struct BuildingDescription {
= pub transform: Transform,
= pub variant: BuildingVariant,
= pub address: String,
- pub capacity: i16,
+ pub capacity: HousingCapacity,
+ pub residents: Option<HashSet<PersonId>>,
=}
=
=/// A tag for building entities
@@ -154,6 +204,7 @@ pub struct BuildingBundle {
= scene: SceneBundle,
= name: Name,
= class: BuildingClass,
+ capacity: HousingCapacity,
= marker: Building,
=}
=
@@ -164,7 +215,8 @@ impl BuildingBundle {
= address,
= variant,
= transform,
- ..
+ capacity,
+ residents: _,
= } = description;
= let scene = scenes.0.get(&variant).unwrap().clone();
= let name = address.into();
@@ -179,6 +231,7 @@ impl BuildingBundle {
= transform,
= ..default()
= },
+ capacity,
= marker: Building,
= }
= }
@@ -187,16 +240,27 @@ impl BuildingBundle {
=#[derive(Component, Hash, PartialEq, Eq, Clone, Debug, From, Display, Reflect)]
=pub struct BuildingId(Uuid);
=
-// TODO: Keep track of houses with residents, empty, and full
-// pub struct Residents(Vec<PersonId>)
+/// Who lives in the building?
+///
+/// Take care to keep in sync with persons::Residence component.
+#[derive(Component, Debug, Clone, Reflect, From)]
+pub struct Residents(pub HashSet<PersonId>);
=
-/// How many more people can this building house?
+/// How many people can this building house in total?
=#[derive(Component, Debug, Clone, Reflect)]
=pub struct HousingCapacity(pub u8);
=
-/// A negative capacity
+/// Marker for buildings with more residents than capacity
+///
+/// When residents change, take care to update it if necessary.
+#[derive(Component, Debug, Clone, Reflect)]
+pub struct IsOvercrowded;
+
+/// Marker for buildings with less residents than capacity
+///
+/// When residents change, take care to update it if necessary.
=#[derive(Component, Debug, Clone, Reflect)]
-pub struct Overcrowded(pub u8);
+pub struct HasSpareCapacity;
=
=/// A variant identifies the building scene
=///
@@ -286,9 +350,6 @@ fn order_construction(
= let district_name = districts.get(**parent).unwrap().to_string();
= let building_number = number.0.to_string();
= let address = format!("{building_number} {district_name}");
- info!("Building a new house {address}");
-
- // Remove parcel
= commands.entity(entity).despawn();
=
= // Order the construction
@@ -297,8 +358,10 @@ fn order_construction(
= address,
= transform: transform.to_owned().into(),
= variant: variant.clone(),
- capacity: 2,
+ capacity: HousingCapacity(2),
+ residents: None,
= };
+ info!("Building a new house {description:#?}");
= build_events.send(ConstructionOrder(description));
= }
=}
@@ -321,24 +384,25 @@ fn construct_building(
= scenes: Res<BuildingScenes>,
= mut register: ResMut<BuildingsRegister>,
=) {
- let Vec3 { x, z, .. } = description.transform.translation;
-
- debug!(
- "Spawning a new {variant} building at ({x:.2}, {z:.2})!",
- variant = &description.variant
- );
-
+ info!("Constructing {description:#?}",);
= let id = description.id.clone();
- let capacity = description.capacity;
+ let HousingCapacity(capacity) = description.capacity.clone();
+ let residents = description.residents.clone().unwrap_or_default();
+
= let mut entity = commands.spawn(BuildingBundle::new(description, &scenes));
=
- if capacity > 0 {
- entity.insert(HousingCapacity(capacity as u8));
- } else if capacity < 0 {
- entity.insert(Overcrowded(capacity.saturating_abs() as u8));
+ if residents.len() > capacity as usize {
+ entity.insert(IsOvercrowded);
= };
+ if residents.len() < capacity as usize {
+ entity.insert(HasSpareCapacity);
+ };
+
+ let entity = entity.id();
=
- register.0.insert(id, entity.id());
+ info!("Registering the new house {id:#?} -> {entity:#?}");
+
+ register.0.insert(id, entity);
=}
=
=pub type BuildingsSnapshot = Vec<BuildingDescription>;
@@ -350,29 +414,20 @@ impl Snapshot for BuildingsSnapshot {
= &Name,
= &BuildingVariant,
= &Transform,
- Option<&HousingCapacity>,
- Option<&Overcrowded>,
+ &HousingCapacity,
+ Option<&Residents>,
= ), With<Building>>()
= .iter(&world)
- .map(|(id, name, variant, transform, capacity, overcrowded)| {
- let capacity = match (capacity, overcrowded) {
- (None, None) => 0,
- (None, Some(overcrowded)) => (overcrowded.0 as i16).neg(),
- (Some(capacity), None) => capacity.0 as i16,
- (Some(_), Some(_)) => {
- panic!(
- "A house at {name} have capacity and is overcrowded at the same time!"
- )
- }
- };
- BuildingDescription {
+ .map(
+ |(id, name, variant, transform, capacity, residents)| BuildingDescription {
= id: id.clone(),
= address: name.to_string(),
= variant: variant.clone(),
= transform: *transform,
- capacity,
- }
- })
+ capacity: capacity.clone(),
+ residents: residents.map(|component| component.0.clone()),
+ },
+ )
= .collect()
= }
=index 63e6c02..866f9d7 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -512,11 +512,11 @@ fn plan_new_districts(
= .into_iter()
= })
= .filter(|candidate| {
- info!("Trying to place a district {candidate:?}");
+ debug!("Trying to place a district {candidate:?}");
=
= // TODO: Extract into Latitude::same_hemisphere method
= if i32::from(&candidate.east()) * i32::from(&candidate.west()) < 0 {
- info!("District would cross the latitudinal highway");
+ debug!("District would cross the latitudinal highway");
= return false;
= }
= if i32::from(&candidate.north()) * i32::from(&candidate.south()) < 0 {
@@ -527,7 +527,7 @@ fn plan_new_districts(
= let outer_buffer = ((candidate.length() + candidate.width()) * 10) as f32;
= let max_distance = settings.land_radius - outer_buffer;
= if candidate.center().length() > max_distance {
- info!("District would be too close to the edge.");
+ debug!("District would be too close to the edge.");
= return false;
= }
=
@@ -542,7 +542,7 @@ fn plan_new_districts(
= return;
= };
=
- info!("New district established: {selected:?}");
+ info!("New district established: {selected:#?}");
= new_districts.send(NewDistrictEstablished(selected));
=}
=index d72ef71..205ed0a 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -6,7 +6,7 @@ use crate::districts::{DistrictsSystems, NewDistrictEstablished};
=use crate::population::{Immigrated, MovedIn, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
=use crate::GameState;
-use bevy::ecs::system::SystemId;
+use bevy::ecs::system::{RunSystemOnce, SystemId};
=use bevy::prelude::*;
=use std::fmt::Display;
=use std::ops::DerefMut;
@@ -19,7 +19,11 @@ impl Plugin for HistoryPlugin {
= .init_resource::<Future>()
= .add_systems(Startup, setup_history_systems)
= .add_systems(OnEnter(GameState::Simulate), take_snapshot)
- .add_systems(PreUpdate, take_snapshot.run_if(on_event::<NewMonth>()))
+ .add_systems(
+ PreUpdate,
+ take_snapshot
+ .run_if(in_state(GameState::Simulate).and_then(on_event::<NewMonth>())),
+ )
= .add_systems(
= Update,
= register_historical_events.run_if(in_state(GameState::Simulate)),
@@ -85,11 +89,12 @@ impl Display for HistoricalEvent {
= transform,
= variant,
= capacity,
+ residents,
= })) => {
= let Vec3 { x, z, .. } = transform.translation;
= write!(
= f,
- "Construction of a new building ({variant}, {id}) with a capacity of {capacity} ordered at {address} ({x:.2}, {z:.2})",
+ "Construction of a new building ({variant}, {id}) with a {capacity:?} and {residents:?} residents ordered at {address} ({x:.2}, {z:.2})" ,
= )
= }
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {
@@ -116,25 +121,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: {event}");
-
+ info!("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: {event}");
-
+ info!("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: {event}");
-
+ info!("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: {event}");
+ info!("Registering a historical event on {date:?}: {event:#?}");
= history.events.push(EventLogEntry { event, date });
= }
=}
@@ -184,11 +186,12 @@ pub fn take_snapshot(world: &mut World) {
= world.resource_scope(|_, mut history: Mut<History>| {
= history.deref_mut().snapshots.push(snapshot);
= let count = history.snapshots.len();
- info!("Registering snapshot {count} on {timestamp}.")
+ info!("Registering snapshot {count} on {date:#?}.")
= })
=}
=
=fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
+ info!("Restoring {snapshot:#?}");
= // TODO: DRY on rollback. Maybe a macro?
= world.resource_scope(|_, mut game_state: Mut<NextState<GameState>>| {
= game_state.set(GameState::Explore);
@@ -239,6 +242,8 @@ fn replay_historical_events(
=) {
= future.events.retain(|entry| {
= if entry.date <= date.0 {
+ info!("Replaying a historical event {entry:#?} (now is {date:#?})");
+
= match entry.event.clone() {
= HistoricalEvent::ConstructionOrder(order) => {
= orders.send(order);index 0c24f7e..985e535 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -39,8 +39,10 @@ impl Display for History {
= transform,
= variant,
= capacity,
+ residents: _,
= })) => {
= let Vec3 { x, z, .. } = transform.translation;
+ let capacity = capacity.0;
=
= writeln!(f, "Insert into buildings (")?;
= writeln!(f, " id,")?;index 9834d04..e199334 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,4 +1,4 @@
-use crate::buildings::{BuildingId, BuildingsRegister, HousingCapacity, Overcrowded};
+use crate::buildings::{BuildingId, HasSpareCapacity};
=use crate::date;
=use crate::date::SimulationFrameCounter;
=use crate::history::Snapshot;
@@ -134,7 +134,7 @@ fn implement_immigration(
= systems: Res<PopulationSystems>,
=) {
= for Immigrated(person) in immigrated.read().cloned() {
- info!("New immigrant {person:?} ");
+ info!("New immigrant {person:#?} ");
= commands.run_system_with_input(systems.setup_new_person, person);
= }
=}
@@ -148,12 +148,12 @@ pub struct MovedIn {
=// For now just assign random buildings for each homeless person, with a 50% chance
=fn search_for_houses(
= persons: Query<&PersonId, Without<Residence>>,
- buildings: Query<(&BuildingId, &HousingCapacity)>,
+ buildings: Query<&BuildingId, With<HasSpareCapacity>>,
= mut moved_in: EventWriter<MovedIn>,
=) {
= for person_id in persons.iter() {
= if random() {
- if let Some((building_id, capacity)) = buildings.iter().choose(&mut thread_rng()) {
+ if let Some(building_id) = buildings.iter().choose(&mut thread_rng()) {
= moved_in.send(MovedIn {
= who: person_id.clone(),
= where_to: building_id.clone(),
@@ -165,23 +165,17 @@ fn search_for_houses(
=
=fn move_into_houses(
= mut moved_in: EventReader<MovedIn>,
- mut buildings: Query<&mut HousingCapacity>,
- buildings_register: Res<BuildingsRegister>,
= persons_register: Res<PersonsRegister>,
= mut commands: Commands,
=) {
- for MovedIn { who, where_to } in moved_in.read() {
- let person = persons_register.0.get(who).unwrap();
- let building = buildings_register.0.get(where_to).unwrap();
- let mut capacity = buildings.get_mut(*building).unwrap();
+ for event in moved_in.read() {
+ info!("Moving into a house {event:#?}");
=
+ let MovedIn { who, where_to } = event;
+ let Some(person) = persons_register.0.get(who) else {
+ panic!("Person {who:#?} not in the register! {persons_register:#?}");
+ };
= commands.entity(*person).insert(Residence(where_to.clone()));
-
- // TODO: Consider overcrowding
- capacity.0 = capacity.0.saturating_sub(1);
- if capacity.0 == 0 {
- commands.entity(*building).remove::<HousingCapacity>();
- }
= }
=}
=
@@ -312,6 +306,7 @@ impl Snapshot for PopulationSnapshot {
=
= for person in self.persons.iter() {
= world.resource_scope(|world, systems: Mut<PopulationSystems>| {
+ info!("Restoring {person:#?}");
= world
= .run_system_with_input(systems.setup_new_person, person.clone())
= .unwrap()index 2b9f87a..b466c7c 100644
--- a/src/population_ui.rs
+++ b/src/population_ui.rs
@@ -58,7 +58,7 @@ fn paint_ui(
=
= for (id, name, sex, Savings(savings), residence) in persons.iter() {
= let address = residence
- .map(|Residence(id)| buildings_register.0.get(id).unwrap())
+ .and_then(|Residence(id)| buildings_register.0.get(id))
= .map(|entity| buildings.get(*entity).unwrap().as_str())
= .unwrap_or("-");
=Make sure future events don't leak to the past
On by
In re-play mode it's possible that event's might be emitted right before a user initiated rollback. Then they would be processed after the rollback, when simulation is "in the past" relative to those events. So they are "from the future". This would be bad and could lead to errors. I'm not sure if it was actually a problem, but just in case rollback now clears all pending historical events. It might also be a problem for other (non-historical) events, but I don't know how to deal with those.
index 205ed0a..2cc21fc 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -192,6 +192,10 @@ pub fn take_snapshot(world: &mut World) {
=
=fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= info!("Restoring {snapshot:#?}");
+ // Make sure no pending events from the future are going to reach systems
+ // after the rollback
+ world.run_system_once(clear_pending_historical_events);
+
= // TODO: DRY on rollback. Maybe a macro?
= world.resource_scope(|_, mut game_state: Mut<NextState<GameState>>| {
= game_state.set(GameState::Explore);
@@ -222,6 +226,18 @@ fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= });
=}
=
+fn clear_pending_historical_events(
+ mut construction_orders: EventReader<ConstructionOrder>,
+ mut new_districts: EventReader<NewDistrictEstablished>,
+ mut immigrated: EventReader<Immigrated>,
+ mut moved_in: EventReader<MovedIn>,
+) {
+ construction_orders.clear();
+ new_districts.clear();
+ immigrated.clear();
+ moved_in.clear();
+}
+
=#[derive(Debug, Resource)]
=pub struct HistorySystems {
= pub rollback: SystemId<MainSnapshot>,Revert simulation duration to 2 years
On by
...in development. I believe it was changed by mistake while experimenting.
index b5525d6..1f5f2b2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -112,7 +112,7 @@ impl Default for SimulationParameters {
= sun_elevation: 600.,
= first_year: 1865,
= #[cfg(debug_assertions)]
- years: 20,
+ years: 2,
= #[cfg(not(debug_assertions))]
= years: 150,
= frame_offset: 2.0 * HOUR,Remove an unused variable
On by
index 2cc21fc..5c191be 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -173,8 +173,6 @@ pub fn take_snapshot(world: &mut World) {
= let buildings = BuildingsSnapshot::capture(world);
= let population = PopulationSnapshot::capture(world);
=
- let timestamp: Timestamp = date.into();
-
= let snapshot = MainSnapshot {
= date,
= buildings,Use Snapshot trait for districts
On by
index 866f9d7..2e01928 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,6 +1,7 @@
=use crate::buildings::Building;
=use crate::coordinates::{Coordinate, Coordinates, Direction, Latitude, Longitude};
=use crate::date::SimulationFrameCounter;
+use crate::history::Snapshot;
=use crate::names::{DISTRICT_NAMES, DISTRICT_PREFIXES};
=use crate::parcels::{setup_parcels, Parcel};
=use crate::population::Person;
@@ -578,24 +579,29 @@ fn implement_new_districts(
= }
=}
=
-pub type Snapshot = Vec<DistrictDescription>;
+#[derive(Debug, Clone)]
+pub struct DistrictsSnapshot(Vec<DistrictDescription>);
=
-pub fn take_snapshot(districts: Query<&DistrictDescription>) -> Snapshot {
- districts.iter().cloned().collect()
-}
-
-fn rollback(
- In(snapshot): In<Snapshot>,
- districts: Query<Entity, With<DistrictDescription>>,
- mut commands: Commands,
- systems: Res<DistrictsSystems>,
-) {
- for entity in districts.iter() {
- commands.entity(entity).despawn_recursive();
+impl Snapshot for DistrictsSnapshot {
+ fn capture(world: &mut World) -> Self {
+ let mut districts = world.query::<&DistrictDescription>();
+ Self(districts.iter(&world).cloned().collect())
= }
=
- for district in snapshot {
- commands.run_system_with_input(systems.construct_district, district.to_owned());
+ fn restore(&self, world: &mut World) {
+ let mut districts = world.query_filtered::<Entity, With<DistrictDescription>>();
+ let entities = districts.iter(world).collect_vec();
+ for entity in entities {
+ despawn_with_children_recursive(world, entity);
+ }
+
+ let construct_district =
+ world.resource_scope(|_, systems: Mut<DistrictsSystems>| systems.construct_district);
+ for district in self.0.iter() {
+ world
+ .run_system_with_input(construct_district, district.to_owned())
+ .unwrap();
+ }
= }
=}
=
@@ -603,21 +609,13 @@ fn rollback(
=
=#[derive(Resource, Debug)]
=pub struct DistrictsSystems {
- pub take_snapshot: SystemId<(), Snapshot>,
- pub rollback: SystemId<Snapshot>,
= pub construct_district: SystemId<DistrictDescription>,
=}
=
=fn register_district_systems(world: &mut World) {
- let take_snapshot = world.register_system(take_snapshot);
- let rollback = world.register_system(rollback);
= let construct_district = world.register_system(construct_district);
=
- world.insert_resource(DistrictsSystems {
- take_snapshot,
- rollback,
- construct_district,
- });
+ world.insert_resource(DistrictsSystems { construct_district });
=}
=
=fn construct_district(index 5c191be..f28a61c 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,8 +1,8 @@
=use crate::buildings::{self, BuildingDescription, BuildingsSnapshot, ConstructionOrder};
=use crate::date::{Date, DateSystems, NewMonth};
=use crate::day_month::{DayMonth, Month};
-use crate::districts;
-use crate::districts::{DistrictsSystems, NewDistrictEstablished};
+use crate::districts::NewDistrictEstablished;
+use crate::districts::{self, DistrictsSnapshot};
=use crate::population::{Immigrated, MovedIn, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
=use crate::GameState;
@@ -151,7 +151,7 @@ pub struct MainSnapshot {
=
= // TODO: Replace all below with a single Vec<Box<dyn Snapshot>>,
= buildings: BuildingsSnapshot,
- districts: districts::Snapshot,
+ districts: DistrictsSnapshot,
= roads: RoadsSnapshot,
= population: PopulationSnapshot,
=}
@@ -165,11 +165,7 @@ pub fn take_snapshot(world: &mut World) {
= };
=
= let roads = RoadsSnapshot::capture(world);
- let districts = {
- let system =
- world.resource_scope(|_, systems: Mut<DistrictsSystems>| systems.take_snapshot);
- world.run_system(system).unwrap()
- };
+ let districts = DistrictsSnapshot::capture(world);
= let buildings = BuildingsSnapshot::capture(world);
= let population = PopulationSnapshot::capture(world);
=
@@ -204,12 +200,7 @@ fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= world.run_system_with_input(system, snapshot.date).unwrap();
= }
= snapshot.roads.restore(world);
- {
- let system = world.resource_scope(|_, systems: Mut<DistrictsSystems>| systems.rollback);
- world
- .run_system_with_input(system, snapshot.districts)
- .unwrap();
- }
+ snapshot.districts.restore(world);
= snapshot.buildings.restore(world);
= snapshot.population.restore(world);
=Use Snapshot trait for date (the last one)
On by
Now all snapshots are being captured and restored using the trait. Because it's synchronous I hope it will be less error prone.
index 15c75d3..309ccec 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -1,4 +1,5 @@
=use crate::day_month::{DayMonth, HOUR, MINUTE};
+use crate::history::Snapshot;
=use crate::{GameState, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
@@ -28,7 +29,7 @@ impl Plugin for DatePlugin {
= }
=}
=
-#[derive(Resource, Debug)]
+#[derive(Resource, Debug, Clone)]
=pub struct Date(pub DayMonth);
=
=impl Date {
@@ -132,33 +133,28 @@ fn update_date_display(mut display: Query<&mut Text, With<DateDisplay>>, date: R
= display.sections[0].value = date.0.to_string();
=}
=
-pub type Snapshot = DayMonth;
-
-pub fn take_snapshot(date: Res<Date>) -> Snapshot {
- date.0
-}
+pub type DateSnapshot = Date;
=
-fn rollback(In(snapshot): In<Snapshot>, mut date: ResMut<Date>) {
- date.0 = snapshot
+impl Snapshot for Date {
+ fn capture(world: &mut World) -> Self {
+ world.resource_scope(|_, date: Mut<Date>| date.clone())
+ }
+ fn restore(&self, world: &mut World) {
+ world.resource_scope(|_, mut date: Mut<Date>| *date = self.clone());
+ }
=}
=
=// Systems exposed to other systems
=
=#[derive(Resource, Debug)]
=pub struct DateSystems {
- pub take_snapshot: SystemId<(), Snapshot>,
- pub rollback: SystemId<Snapshot>,
= pub advance_simulation_date: SystemId,
=}
=
=fn register_date_systems(world: &mut World) {
- let take_snapshot = world.register_system(take_snapshot);
- let rollback = world.register_system(rollback);
= let advance_simulation_date = world.register_system(advance_simulation_date);
=
= world.insert_resource(DateSystems {
- take_snapshot,
- rollback,
= advance_simulation_date,
= });
=}index f28a61c..cc3088b 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,5 +1,5 @@
=use crate::buildings::{self, BuildingDescription, BuildingsSnapshot, ConstructionOrder};
-use crate::date::{Date, DateSystems, NewMonth};
+use crate::date::{Date, DateSnapshot, NewMonth};
=use crate::day_month::{DayMonth, Month};
=use crate::districts::NewDistrictEstablished;
=use crate::districts::{self, DistrictsSnapshot};
@@ -147,7 +147,7 @@ fn register_historical_events(
=/// The main snapshot that binds them all
=#[derive(Clone, Debug)]
=pub struct MainSnapshot {
- date: DayMonth,
+ date: Date,
=
= // TODO: Replace all below with a single Vec<Box<dyn Snapshot>>,
= buildings: BuildingsSnapshot,
@@ -159,16 +159,14 @@ pub struct MainSnapshot {
=// TODO: Find a way to coordinate with lay_initial_roads without exposing this system
=pub fn take_snapshot(world: &mut World) {
= // TODO: DRY on take_snapshot. Maybe a macro?
- let date = {
- let system = world.resource_scope(|_, systems: Mut<DateSystems>| systems.take_snapshot);
- world.run_system(system).unwrap()
- };
-
+ let date = DateSnapshot::capture(world);
= let roads = RoadsSnapshot::capture(world);
= let districts = DistrictsSnapshot::capture(world);
= let buildings = BuildingsSnapshot::capture(world);
= let population = PopulationSnapshot::capture(world);
=
+ info!("Registering a new snapshot on {date:#?}");
+
= let snapshot = MainSnapshot {
= date,
= buildings,
@@ -179,8 +177,6 @@ pub fn take_snapshot(world: &mut World) {
=
= world.resource_scope(|_, mut history: Mut<History>| {
= history.deref_mut().snapshots.push(snapshot);
- let count = history.snapshots.len();
- info!("Registering snapshot {count} on {date:#?}.")
= })
=}
=
@@ -190,15 +186,12 @@ fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= // after the rollback
= world.run_system_once(clear_pending_historical_events);
=
- // TODO: DRY on rollback. Maybe a macro?
= world.resource_scope(|_, mut game_state: Mut<NextState<GameState>>| {
= game_state.set(GameState::Explore);
= });
=
- {
- let system = world.resource_scope(|_, systems: Mut<DateSystems>| systems.rollback);
- world.run_system_with_input(system, snapshot.date).unwrap();
- }
+ // TODO: DRY on rollback. Maybe a macro?
+ snapshot.date.restore(world);
= snapshot.roads.restore(world);
= snapshot.districts.restore(world);
= snapshot.buildings.restore(world);
@@ -210,7 +203,7 @@ fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
= future.events = events
= .clone()
= .into_iter()
- .filter(|event| event.date >= snapshot.date)
+ .filter(|event| event.date >= snapshot.date.0)
= .collect();
= });
=}Fix a variable name
On by
Copy paste much.
index 38711a5..41d0a62 100644
--- a/src/exploration.rs
+++ b/src/exploration.rs
@@ -77,8 +77,8 @@ fn paint_ui(
=
= let next_speed = (time.relative_speed() * 2.0).min(128.0);
= let label = format!("⏩ ×{next_speed:.0}");
- let export_button = ui.add(egui::Button::new(label).min_size(min_button_size));
- if export_button.clicked() {
+ let speedup_button = ui.add(egui::Button::new(label).min_size(min_button_size));
+ if speedup_button.clicked() {
= time.unpause();
= time.set_relative_speed(next_speed);
= }Implement history export in RON format
On by
Most of the changes is deriving Serialize and Deserialize for all the types that are used in History. Then in exploration.rs instead of crappy pseudo-SQL the program prints RON to the console (or standard output). Next step is to make it download or save a generated file.
index 91487cb..d92bf87 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -476,6 +476,7 @@ dependencies = [
= "bevy_tasks",
= "bevy_utils",
= "bytemuck",
+ "serde",
=]
=
=[[package]]
@@ -688,6 +689,7 @@ dependencies = [
= "bevy_math",
= "bevy_reflect",
= "bevy_utils",
+ "serde",
= "smol_str",
= "thiserror",
=]
@@ -1000,6 +1002,7 @@ dependencies = [
= "bevy_reflect",
= "bevy_utils",
= "crossbeam-channel",
+ "serde",
= "thiserror",
=]
=
@@ -1014,6 +1017,7 @@ dependencies = [
= "bevy_hierarchy",
= "bevy_math",
= "bevy_reflect",
+ "serde",
= "thiserror",
=]
=
@@ -1041,6 +1045,7 @@ dependencies = [
= "bevy_utils",
= "bevy_window",
= "bytemuck",
+ "serde",
= "taffy",
= "thiserror",
=]
@@ -1089,6 +1094,7 @@ dependencies = [
= "bevy_reflect",
= "bevy_utils",
= "raw-window-handle 0.6.0",
+ "serde",
= "smol_str",
=]
=
@@ -2861,6 +2867,8 @@ dependencies = [
= "js-sys",
= "rand",
= "regex",
+ "ron",
+ "serde",
= "wasm-bindgen",
=]
=index 698fa47..dc68651 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2021"
=# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
=
=[dependencies]
-bevy = { version = "0.13", features = ["wayland"] }
+bevy = { version = "0.13", features = ["wayland", "serialize"] }
=bevy-inspector-egui = { git = "https://github.com/jakobhellermann/bevy-inspector-egui", rev = "1563fe9", version = "0.23.4" }
=bevy_egui = "0.25.0"
=bevy_panorbit_camera = "0.16.0"
@@ -15,6 +15,8 @@ itertools = "0.12.1"
=js-sys = "0.3.68"
=rand = "0.8.5"
=regex = "1.10.3"
+ron = "0.8.1"
+serde = "1.0.197"
=wasm-bindgen = "0.2.91"
=
=[profile.dev.package."*"]index 8d2a09f..022a89a 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -14,6 +14,7 @@ use itertools::Itertools;
=use rand::seq::IteratorRandom;
=use rand::{thread_rng, Rng};
=use regex::Regex;
+use serde::{Deserialize, Serialize};
=use std::fmt::Display;
=use std::str::FromStr;
=
@@ -180,10 +181,10 @@ fn setup_building_scenes(
=/// This event represents a decision to construct a new building.
=///
=/// It will be stored in the history, and will result in spawning a new building.
-#[derive(Event, Clone, Debug)]
+#[derive(Event, Clone, Debug, Serialize, Deserialize)]
=pub struct ConstructionOrder(pub BuildingDescription);
=
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
=pub struct BuildingDescription {
= pub id: BuildingId,
= pub transform: Transform,
@@ -194,7 +195,7 @@ pub struct BuildingDescription {
=}
=
=/// A tag for building entities
-#[derive(Component)]
+#[derive(Component, Serialize, Deserialize)]
=pub struct Building;
=
=#[derive(Bundle)]
@@ -237,17 +238,19 @@ impl BuildingBundle {
= }
=}
=
-#[derive(Component, Hash, PartialEq, Eq, Clone, Debug, From, Display, Reflect)]
+#[derive(
+ Component, Hash, PartialEq, Eq, Clone, Debug, From, Display, Reflect, Serialize, Deserialize,
+)]
=pub struct BuildingId(Uuid);
=
=/// Who lives in the building?
=///
=/// Take care to keep in sync with persons::Residence component.
-#[derive(Component, Debug, Clone, Reflect, From)]
+#[derive(Component, Debug, Clone, Reflect, From, Serialize, Deserialize)]
=pub struct Residents(pub HashSet<PersonId>);
=
=/// How many people can this building house in total?
-#[derive(Component, Debug, Clone, Reflect)]
+#[derive(Component, Debug, Clone, Reflect, Serialize, Deserialize)]
=pub struct HousingCapacity(pub u8);
=
=/// Marker for buildings with more residents than capacity
@@ -266,7 +269,7 @@ pub struct HasSpareCapacity;
=///
=/// Variants come from the buildings.blend file (and buildings.glb exported from
=/// Blender). They are parsed from scene names.
-#[derive(Component, Debug, Clone, Hash, PartialEq, Eq, Reflect)]
+#[derive(Component, Debug, Clone, Hash, PartialEq, Eq, Reflect, Serialize, Deserialize)]
=pub struct BuildingVariant {
= pub class: String,
=index 81bc1ea..3ec17bb 100644
--- a/src/coordinates.rs
+++ b/src/coordinates.rs
@@ -1,5 +1,6 @@
=use bevy::math::{Vec2, Vec3};
=use derive_more::{AsRef, From};
+use serde::{Deserialize, Serialize};
=use std::borrow::Borrow;
=use std::fmt::Display;
=use std::marker::PhantomData;
@@ -57,14 +58,16 @@ pub struct Longitude;
=/// assert_eq!(x, y);
=/// ```
=///
-#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
+#[derive(
+ Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Serialize, Deserialize,
+)]
=pub struct Coordinate<T>(i32, PhantomData<T>)
=where
= T: Dimension + Clone + Ord + PartialOrd + Eq + PartialEq;
=
=/// First element is longitude from west to east, second is latitude from north to south
=// TODO: Specify Latitude and Longitude types for safety
-#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, AsRef)]
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, AsRef, Serialize, Deserialize)]
=pub struct Coordinates(Coordinate<Longitude>, Coordinate<Latitude>);
=
=#[derive(Copy, Clone, PartialEq, Eq)]index 309ccec..f23bd36 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -5,6 +5,7 @@ use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
=use bevy_inspector_egui::prelude::*;
+use serde::{Deserialize, Serialize};
=
=pub struct DatePlugin;
=
@@ -29,7 +30,7 @@ impl Plugin for DatePlugin {
= }
=}
=
-#[derive(Resource, Debug, Clone)]
+#[derive(Resource, Debug, Clone, Serialize, Deserialize)]
=pub struct Date(pub DayMonth);
=
=impl Date {index 81aa8a8..461c87c 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -1,5 +1,7 @@
=use std::fmt::Display;
=
+use serde::{Deserialize, Serialize};
+
=pub const HOUR: f32 = 1.0 / 24.0;
=pub const MINUTE: f32 = HOUR / 60.0;
=
@@ -74,7 +76,7 @@ mod month_tests {
=}
=
=/// Represents a month in a day
-#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
+#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
=pub struct DayMonth {
= /// The current year
= year: i32,index 2e01928..b660eec 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -15,6 +15,7 @@ use itertools::Itertools;
=use rand::seq::{IteratorRandom, SliceRandom};
=use rand::thread_rng;
=use regex::Regex;
+use serde::{Deserialize, Serialize};
=use std::borrow::Borrow;
=use std::f32::consts::FRAC_PI_2;
=use std::iter::{self, once};
@@ -159,7 +160,7 @@ impl DistrictBundle {
=#[derive(Component, Debug, Clone, Copy)]
=pub struct District;
=
-#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
=pub struct DistrictVariant {
= /// The length of the district model
= ///
@@ -238,12 +239,12 @@ struct DistrictScenes(HashMap<DistrictVariant, Handle<Scene>>);
=/// This event represents a decision to construct a new building.
=///
=/// It will be stored in the history, and will result in spawning a new building.
-#[derive(Event, Clone, Debug)]
+#[derive(Event, Clone, Debug, Serialize, Deserialize)]
=pub struct NewDistrictEstablished(pub DistrictDescription);
=
=// TODO: Divide into smaller components.
=/// A logical description of the district, used for planning and snapshotting
-#[derive(Component, Debug, Clone)]
+#[derive(Component, Debug, Clone, Serialize, Deserialize)]
=pub struct DistrictDescription {
= /// Coordinates of the north-west corner, after the rotation
= pub origin: Coordinates,
@@ -258,7 +259,7 @@ pub struct DistrictDescription {
= pub name: Name,
=}
=
-#[derive(Component, Debug, Clone, Copy)]
+#[derive(Component, Debug, Clone, Copy, Serialize, Deserialize)]
=pub enum Rotation {
= None = 0,
= CW90,
@@ -579,7 +580,7 @@ fn implement_new_districts(
= }
=}
=
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
=pub struct DistrictsSnapshot(Vec<DistrictDescription>);
=
=impl Snapshot for DistrictsSnapshot {index 41d0a62..009d88d 100644
--- a/src/exploration.rs
+++ b/src/exploration.rs
@@ -90,7 +90,12 @@ fn paint_ui(
=
= let export_button = ui.add(egui::Button::new("⏏").min_size(min_button_size));
= if export_button.clicked() {
- let exported = pgsql_export::export(history.as_ref());
+ // let exported = pgsql_export::export(history.as_ref());
+ let exported = ron::ser::to_string_pretty(
+ history.as_ref(),
+ ron::ser::PrettyConfig::default(),
+ )
+ .unwrap();
= // TODO: Save a file
= info!("Export:\n\n{exported}");
= }index cc3088b..852e363 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,13 +1,14 @@
-use crate::buildings::{self, BuildingDescription, BuildingsSnapshot, ConstructionOrder};
+use crate::buildings::{BuildingDescription, BuildingsSnapshot, ConstructionOrder};
=use crate::date::{Date, DateSnapshot, NewMonth};
=use crate::day_month::{DayMonth, Month};
+use crate::districts::DistrictsSnapshot;
=use crate::districts::NewDistrictEstablished;
-use crate::districts::{self, DistrictsSnapshot};
=use crate::population::{Immigrated, MovedIn, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
=use crate::GameState;
=use bevy::ecs::system::{RunSystemOnce, SystemId};
=use bevy::prelude::*;
+use serde::{Deserialize, Serialize};
=use std::fmt::Display;
=use std::ops::DerefMut;
=
@@ -35,7 +36,7 @@ impl Plugin for HistoryPlugin {
= }
=}
=
-#[derive(Resource, Default, Debug)]
+#[derive(Resource, Default, Debug, Serialize, Deserialize)]
=pub struct History {
= pub events: Vec<EventLogEntry>,
= pub snapshots: Vec<MainSnapshot>,
@@ -65,16 +66,16 @@ pub trait Snapshot {
=}
=
=/// A wrapper for any kind of event that should be replayed in explore mode
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
=pub enum HistoricalEvent {
- ConstructionOrder(buildings::ConstructionOrder),
- NewDistrictEstablished(districts::NewDistrictEstablished),
+ ConstructionOrder(ConstructionOrder),
+ NewDistrictEstablished(NewDistrictEstablished),
= Immigrated(Immigrated),
= MovedIn(MovedIn),
=}
=
=/// A historical event together with it's date
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
=pub struct EventLogEntry {
= pub date: DayMonth,
= pub event: HistoricalEvent,
@@ -145,7 +146,7 @@ fn register_historical_events(
=// See https://github.com/hankjordan/bevy_save/blob/6fe1d55c822ecd075db9a86d383042c9fa07c192/examples/breakout.rs#L614
=
=/// The main snapshot that binds them all
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
=pub struct MainSnapshot {
= date: Date,
=index e199334..7370353 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -11,6 +11,7 @@ use derive_more::{Display, From};
=use itertools::Itertools;
=use rand::distributions::Standard;
=use rand::prelude::*;
+use serde::{Deserialize, Serialize};
=use std::fmt::Display;
=
=pub struct PopulationPlugin;
@@ -55,7 +56,7 @@ impl Plugin for PopulationPlugin {
=pub struct PersonsRegister(pub HashMap<PersonId, Entity>);
=
=/// A logical description of a person suitable for events and snapshots
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
=pub struct PersonDescription {
= pub id: PersonId,
= pub name: PersonName,
@@ -115,7 +116,7 @@ pub fn setup_new_person(
= register.0.insert(id, entity.id());
=}
=
-#[derive(Event, Debug, Clone, From)]
+#[derive(Event, Debug, Clone, From, Serialize, Deserialize)]
=pub struct Immigrated(pub PersonDescription);
=
=fn plan_immigration(mut immigrated: EventWriter<Immigrated>) {
@@ -139,7 +140,7 @@ fn implement_immigration(
= }
=}
=
-#[derive(Event, Debug, Clone)]
+#[derive(Event, Debug, Clone, Serialize, Deserialize)]
=pub struct MovedIn {
= pub who: PersonId,
= pub where_to: BuildingId,
@@ -214,13 +215,15 @@ impl From<PersonDescription> for PersonBundle {
=#[derive(Component, Debug, Reflect)]
=pub struct Person;
=
-#[derive(Component, Debug, Clone, Hash, PartialEq, Eq, Reflect, From, Display)]
+#[derive(
+ Component, Debug, Clone, Hash, PartialEq, Eq, Reflect, From, Display, Serialize, Deserialize,
+)]
=pub struct PersonId(pub Uuid);
=
-#[derive(Component, Reflect, Clone, Debug, From)]
+#[derive(Component, Reflect, Clone, Debug, From, Serialize, Deserialize)]
=pub struct Savings(pub f32);
=
-#[derive(Component, Reflect, Debug, Clone)]
+#[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)]
=pub enum Sex {
= Male,
= Female,
@@ -245,7 +248,7 @@ impl Display for Sex {
= }
=}
=
-#[derive(Component, Reflect, Debug, Clone)]
+#[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)]
=pub struct PersonName {
= first: String,
= second: String,
@@ -270,7 +273,7 @@ impl Display for PersonName {
=#[derive(Component, Debug, Clone)]
=pub struct Residence(pub BuildingId);
=
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
=pub struct PopulationSnapshot {
= pub persons: Vec<PersonDescription>,
=}index ebb5132..5638035 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -9,6 +9,7 @@ use bevy::utils::hashbrown::HashSet;
=use bevy::utils::HashMap;
=use core::fmt;
=use derive_more::{From, Into};
+use serde::{Deserialize, Serialize};
=use std::borrow::Borrow;
=use std::fmt::Display;
=
@@ -182,7 +183,7 @@ impl Snapshot for RoadsSnapshot {
=///
=/// It's kind of a plan that shows roads that are supposed to be constructed or
=/// a snapshot of existing roads.
-#[derive(Debug, From, Into, Clone, Default)]
+#[derive(Debug, From, Into, Clone, Default, Serialize, Deserialize)]
=pub struct RoadPlan {
= // It holds a HashSet instead of a HashMap, because two road sections can
= // occupy same coordinate, e.g. one N-S and the other E-W. Once constructed
@@ -220,7 +221,7 @@ pub struct Roads {
= sections: HashMap<Coordinates, Entity>,
=}
=
-#[derive(Clone, Copy, Default, PartialEq, Component, Hash, Eq)]
+#[derive(Clone, Copy, Default, PartialEq, Component, Hash, Eq, Serialize, Deserialize)]
=pub struct RoadSection(u8);
=
=impl RoadSection {Extract ron export to own module
On by
The export_pgsql module is now unused. I wrote a comment with an idea what to do with it.
index 009d88d..e68c115 100644
--- a/src/exploration.rs
+++ b/src/exploration.rs
@@ -1,7 +1,7 @@
=use crate::date::Date;
=use crate::history::History;
=use crate::history::HistorySystems;
-use crate::pgsql_export;
+use crate::ron_export;
=use crate::GameState;
=use crate::SimulationParameters;
=use bevy::prelude::*;
@@ -90,14 +90,7 @@ fn paint_ui(
=
= let export_button = ui.add(egui::Button::new("⏏").min_size(min_button_size));
= if export_button.clicked() {
- // let exported = pgsql_export::export(history.as_ref());
- let exported = ron::ser::to_string_pretty(
- history.as_ref(),
- ron::ser::PrettyConfig::default(),
- )
- .unwrap();
- // TODO: Save a file
- info!("Export:\n\n{exported}");
+ ron_export::export(history.as_ref());
= }
= });
=index 1f5f2b2..c32bc7f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -13,6 +13,7 @@ mod pgsql_export;
=mod population;
=mod population_ui;
=mod roads;
+mod ron_export;
=mod simulation;
=mod sun;
=index 985e535..fafb3f7 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -5,6 +5,12 @@ 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()
=}new file mode 100644
index 0000000..fc94dcf
--- /dev/null
+++ b/src/ron_export.rs
@@ -0,0 +1,9 @@
+use crate::history::History;
+use bevy::prelude::*;
+use ron::ser::to_string_pretty;
+
+pub(crate) fn export(history: &History) {
+ let exported = to_string_pretty(history, ron::ser::PrettyConfig::default()).unwrap();
+ // TODO: Save a file
+ info!("Export:\n\n{exported}");
+}Implement exported file download
On by
It works in a web browser. The equivalent for desktop version will write a file to disk, but it's not implemented yet. I want to first make loading the file work.
index d92bf87..256a1bb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -356,9 +356,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_a11y"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bf80cd6d0dca4073f9b34b16f1d187a4caa035fd841892519431783bbc9e287"
+checksum = "6192db480a04d4a0ad5d89a2fbd78ccca5ce902829a49ec2d1dbc213222ed8b1"
=dependencies = [
= "accesskit",
= "bevy_app",
@@ -387,9 +387,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_app"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8bce3544afc010ffed39c136f6d5a9322d20d38df1394d468ba9106caa0434cb"
+checksum = "b508824497f3a3a2fab8398dc3944a4d4adddcc30ee25cd6d45b0a57336549ce"
=dependencies = [
= "bevy_derive",
= "bevy_ecs",
@@ -403,9 +403,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_asset"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac185d8e29c7eb0194f8aae7af3f7234f7ca7a448293be1d3d0d8fef435f65ec"
+checksum = "ccf224b57fb65e1cde921afe0b343c2d595531dbf882c41abad01bbc665a05c4"
=dependencies = [
= "async-broadcast",
= "async-fs",
@@ -435,9 +435,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_asset_macros"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb82d1aac8251378c45a8d0ad788d1bf75f54db28c1750f84f1fd7c00127927a"
+checksum = "684c855651e7734740b76ada0e7daed116c46d393f9031cc45c4fe9ad5829548"
=dependencies = [
= "bevy_macro_utils",
= "proc-macro2",
@@ -465,9 +465,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_core"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7b1b340b8d08f48ecd51b97589d261f5023a7b073d25e300382c49146524103"
+checksum = "e1a8f4722fb978d308b6311f3dd61f6885165055ad05ce3dfc1b2fd001bb017e"
=dependencies = [
= "bevy_app",
= "bevy_ecs",
@@ -503,9 +503,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_derive"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "028ae2a34678055185d7f1beebb1ebe6a8dcf3733e139e4ee1383a7f29ae8ba6"
+checksum = "7de77523d154e220a740e568a89f52fac7de481374bdecbbbeb283a37580ba34"
=dependencies = [
= "bevy_macro_utils",
= "quote",
@@ -530,9 +530,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_ecs"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b85406d5febbbdbcac4444ef61cd9a816f2f025ed692a3fc5439a32153070304"
+checksum = "0a027175613f630a51273c0f8ae909dd54ea3ce72eb573f456056553f79918ac"
=dependencies = [
= "async-channel",
= "bevy_ecs_macros",
@@ -550,9 +550,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_ecs_macros"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a3ce4b65d7c5f1990e729df75cec2ea6e2241b4a0c37b31c281a04c59c11b7b"
+checksum = "55dbbb6300f08cef5983497970db8545d3cbda6ee4f410a6c6742b7b6bbfd3af"
=dependencies = [
= "bevy_macro_utils",
= "proc-macro2",
@@ -576,9 +576,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_encase_derive"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c3d301922e76b16819e17c8cc43b34e92c13ccd06ad19dfa3e52a91a0e13e5c"
+checksum = "df72ac1273fcdb8105736c42815442ae1291f1f577e34cb7e9d18f732103e2f0"
=dependencies = [
= "bevy_macro_utils",
= "encase_derive_impl",
@@ -666,9 +666,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_hierarchy"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9f9f843e43d921f07658c24eae74285efc7a335c87998596f3f365155320c69"
+checksum = "5b2999d1e5bb877b475c9b2d17643d5fb47fc4cc49ea48ba3ab5a6b00ed850a6"
=dependencies = [
= "bevy_app",
= "bevy_core",
@@ -680,9 +680,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_input"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9cb5b2f3747ffb00cf7e3d6b52f7384476921cd31f0cfd3d1ddff31f83d9252"
+checksum = "7c22481e4290e2eca68b0c1f5f0a826f185d8f5e40e05c86bb6044dcfe3a04b3"
=dependencies = [
= "bevy_app",
= "bevy_ecs",
@@ -735,9 +735,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_log"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfd5bcc3531f8008897fb03cc8751b86d0d29ef94f8fd38b422f9603b7ae80d0"
+checksum = "20c7b4e2443654d68b6f8c54e5f1ce3a16c8a9af10f4832390dcae36c1323307"
=dependencies = [
= "android_log-sys",
= "bevy_app",
@@ -751,9 +751,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_macro_utils"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac4401c25b197e7c1455a4875a90b61bba047a9e8d290ce029082c818ab1a21c"
+checksum = "3bbbf88fc577a21ee9994feed2253ee9838b63fb976783b7a549edfbe07c6764"
=dependencies = [
= "proc-macro2",
= "quote",
@@ -764,9 +764,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_math"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f312b1b8aa6d3965b65040b08e33efac030db3071f20b44f9da9c4c3dfcaf76"
+checksum = "9d30721f36a0b5f9ad39deb140c50b85cbbaefebab8d10bd20d9de1c9572f968"
=dependencies = [
= "glam",
= "serde",
@@ -774,9 +774,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_mikktspace"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3075c01f2b1799945892d5310fc1836e47c045dfe6af5878a304a475931a0c5f"
+checksum = "cc081a695c3513f09fdc640bf7f66cd73c47eb479da50312bf9710ee6927729d"
=dependencies = [
= "glam",
=]
@@ -817,15 +817,15 @@ dependencies = [
=
=[[package]]
=name = "bevy_ptr"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86afa4a88ee06b10fe1e6f28a796ba2eedd16804717cbbb911df0cbb0cd6677b"
+checksum = "ea003584000ef02b73800cc7cb62ee74792fff431e6a8df36863c43bf56fb491"
=
=[[package]]
=name = "bevy_reflect"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "133dfab8d403d0575eeed9084e85780bbb449dcf75dd687448439117789b40a2"
+checksum = "b1101dbd44ae35e5c66802e46cfba1182e49f6163c824bee380d4acab5b2f640"
=dependencies = [
= "bevy_math",
= "bevy_ptr",
@@ -841,9 +841,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_reflect_derive"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce1679a4dfdb2c9ff24ca590914c3cec119d7c9e1b56fa637776913acc030386"
+checksum = "e2a8791d5841a6db862571f709d7ee70c2a5eb1634c3a4329817d04f0e307c2d"
=dependencies = [
= "bevy_macro_utils",
= "proc-macro2",
@@ -854,9 +854,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_render"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3b194b7029b7541ef9206ac3cb696d3cb37f70bd3260d293fc00d378547e892"
+checksum = "75bbb48471f8cd06f5253e271f9b793695f5b821fc9d39a875497905578d9867"
=dependencies = [
= "async-channel",
= "bevy_app",
@@ -899,9 +899,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_render_macros"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4aa6d99b50375bb7f63be2c3055dfe2f926f7b3c4db108bb0b1181b4f02766aa"
+checksum = "ffd61a89e7a1b55c78a0aef1fcadd0247fe74a101c00f831791db73d63465051"
=dependencies = [
= "bevy_macro_utils",
= "proc-macro2",
@@ -957,9 +957,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_tasks"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b20f243f6fc4c4ba10c2dbff891e947ddae947bb20b263f43e023558b35294bd"
+checksum = "40be36aeec06b8f0eb87894922c6a7fbd8f2a5c8e77dcb9dcbf77641046988c0"
=dependencies = [
= "async-channel",
= "async-executor",
@@ -993,9 +993,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_time"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9738901b6b251d2c9250542af7002d6f671401fc3b74504682697c5ec822f210"
+checksum = "06fc48cf59acd2b1c52e61787b5bb3db1a0f923cc6ccc68c0d8ab2b5894cfd28"
=dependencies = [
= "bevy_app",
= "bevy_ecs",
@@ -1008,9 +1008,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_transform"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba73744a95bc4b8683e91cea3e79b1ad0844c1d677f31fbbc1814c79a5b4f8f0"
+checksum = "b962ae4253f5413b64a839ab8e8d63bc3e3db45f41d06b6ddc7886acdcb5d0f5"
=dependencies = [
= "bevy_app",
= "bevy_ecs",
@@ -1052,9 +1052,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_utils"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94a06aca1c1863606416b892f4c79e300dbc6211b6690953269051a431c2cca0"
+checksum = "ac758c2e8509a4a260b7a91f920be3beee6ab9e76e388494240ac5d672779159"
=dependencies = [
= "ahash",
= "bevy_utils_proc_macros",
@@ -1071,9 +1071,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_utils_proc_macros"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31ae98e9c0c08b0f5c90e22cd713201f759b98d4fd570b99867a695f8641859a"
+checksum = "014c80f466ed01821a2e602d63cd5076915c1af5de5fa3c074cc4a9ca898ada7"
=dependencies = [
= "proc-macro2",
= "quote",
@@ -1082,9 +1082,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_window"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb627efd7622a61398ac0d3674f93c997cffe16f13c59fb8ae8a05c9e28de961"
+checksum = "fa0c2a1e580b3b0ad0c928a5e250c8375c6a8a70d8b0f483b23d3bf5b670cc1a"
=dependencies = [
= "bevy_a11y",
= "bevy_app",
@@ -1100,9 +1100,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_winit"
-version = "0.13.0"
+version = "0.13.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55105324a201941ae587790f83f6d9caa327e0baa0205558ec41e5ee05a1f703"
+checksum = "a2c408d459172758a4cfa37e3452d4ea0898101ec2b6d92aa3eb698511bef389"
=dependencies = [
= "accesskit_winit",
= "approx",
@@ -2004,6 +2004,41 @@ version = "0.3.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
=
+[[package]]
+name = "gloo-events"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-file"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f"
+dependencies = [
+ "gloo-events",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
=[[package]]
=name = "glow"
=version = "0.13.1"
@@ -2863,6 +2898,8 @@ dependencies = [
= "bevy_egui",
= "bevy_panorbit_camera",
= "derive_more",
+ "gloo-file",
+ "gloo-utils",
= "itertools",
= "js-sys",
= "rand",
@@ -2870,6 +2907,7 @@ dependencies = [
= "ron",
= "serde",
= "wasm-bindgen",
+ "web-sys",
=]
=
=[[package]]
@@ -3956,9 +3994,9 @@ dependencies = [
=
=[[package]]
=name = "web-sys"
-version = "0.3.67"
+version = "0.3.69"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
=dependencies = [
= "js-sys",
= "wasm-bindgen",index dc68651..c7ce3ce 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,8 @@ bevy-inspector-egui = { git = "https://github.com/jakobhellermann/bevy-inspector
=bevy_egui = "0.25.0"
=bevy_panorbit_camera = "0.16.0"
=derive_more = "0.99.17"
+gloo-file = "0.3.0"
+gloo-utils = "0.2.0"
=itertools = "0.12.1"
=js-sys = "0.3.68"
=rand = "0.8.5"
@@ -18,6 +20,7 @@ regex = "1.10.3"
=ron = "0.8.1"
=serde = "1.0.197"
=wasm-bindgen = "0.2.91"
+web-sys = {version = "0.3.69", features = ["HtmlAnchorElement"]}
=
=[profile.dev.package."*"]
=opt-level = "z"index fc94dcf..19e88b5 100644
--- a/src/ron_export.rs
+++ b/src/ron_export.rs
@@ -1,9 +1,45 @@
=use crate::history::History;
-use bevy::prelude::*;
+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();
- // TODO: Save a file
- info!("Export:\n\n{exported}");
+ let file_name = "simulation.ron";
+
+ info!("Saving the simulation to {file_name}");
+ save_file(file_name, exported.as_str());
+}
+
+/// In a web browser, download the file
+#[cfg(target_arch = "wasm32")]
+pub fn save_file(file_name: &str, content: &str) {
+ use gloo_file::{Blob, ObjectUrl};
+ use gloo_utils::document;
+ use wasm_bindgen::prelude::*;
+ use web_sys::HtmlAnchorElement;
+
+ let blob = Blob::new(content);
+ let url = ObjectUrl::from(blob);
+ let href = url.to_string();
+
+ let document = document();
+ let anchor = document
+ .create_element("a")
+ .unwrap()
+ .dyn_into::<HtmlAnchorElement>()
+ .unwrap();
+ anchor.set_attribute("href", &href).unwrap();
+ anchor.set_attribute("download", file_name).unwrap();
+ anchor.click();
+}
+
+/// On a desktop device, save to a file
+#[cfg(not(target_arch = "wasm32"))]
+pub fn save_file(file_name: &str, content: &str) {
+ use bevy::log::error;
+
+ error!(
+ "TODO: Write {length} to a {file_name}",
+ length = content.len()
+ );
=}Redeployed ending of internal paths to follow a 60m grid (starting from road corners)
On by
index c1315db..f6ddc99 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex 17bedf9..2b92be5 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differPOC loading of a simulation
On by
If the program is started with load command line argument like this:
$ cargo run -- --load simulation(6).ron
or URL query parameter, for example:
http://localhost:8080/?load=simulation(6).ron
it will load the specified file from assets/ directory and starts in exploration mode.
index 256a1bb..f1634bb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -169,6 +169,54 @@ dependencies = [
= "libc",
=]
=
+[[package]]
+name = "anstream"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
=[[package]]
=name = "approx"
=version = "0.5.1"
@@ -401,6 +449,21 @@ dependencies = [
= "web-sys",
=]
=
+[[package]]
+name = "bevy_args"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b8e6c0e14e71c792127c44df25006a3355b5da21d539e37749fa29f5a2843d6"
+dependencies = [
+ "bevy",
+ "clap",
+ "console_error_panic_hook",
+ "serde",
+ "serde_qs",
+ "wasm-bindgen",
+ "web-sys",
+]
+
=[[package]]
=name = "bevy_asset"
=version = "0.13.1"
@@ -1358,6 +1421,46 @@ dependencies = [
= "libloading 0.8.3",
=]
=
+[[package]]
+name = "clap"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+
=[[package]]
=name = "clipboard-win"
=version = "5.2.0"
@@ -1383,6 +1486,12 @@ version = "1.1.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
=
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
=[[package]]
=name = "com"
=version = "0.6.0"
@@ -2201,6 +2310,12 @@ dependencies = [
= "winapi",
=]
=
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
=[[package]]
=name = "hexasphere"
=version = "10.0.0"
@@ -2895,8 +3010,10 @@ version = "0.1.0"
=dependencies = [
= "bevy",
= "bevy-inspector-egui",
+ "bevy_args",
= "bevy_egui",
= "bevy_panorbit_camera",
+ "clap",
= "derive_more",
= "gloo-file",
= "gloo-utils",
@@ -2906,6 +3023,8 @@ dependencies = [
= "regex",
= "ron",
= "serde",
+ "serde_qs",
+ "thiserror",
= "wasm-bindgen",
= "web-sys",
=]
@@ -3361,6 +3480,17 @@ dependencies = [
= "serde",
=]
=
+[[package]]
+name = "serde_qs"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c"
+dependencies = [
+ "percent-encoding",
+ "serde",
+ "thiserror",
+]
+
=[[package]]
=name = "sharded-slab"
=version = "0.1.7"
@@ -3464,6 +3594,12 @@ version = "0.1.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
=
+[[package]]
+name = "strsim"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
+
=[[package]]
=name = "svg_fmt"
=version = "0.4.2"
@@ -3774,6 +3910,12 @@ dependencies = [
= "percent-encoding",
=]
=
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
=[[package]]
=name = "uuid"
=version = "1.7.0"index c7ce3ce..0005c94 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,8 +8,10 @@ edition = "2021"
=[dependencies]
=bevy = { version = "0.13", features = ["wayland", "serialize"] }
=bevy-inspector-egui = { git = "https://github.com/jakobhellermann/bevy-inspector-egui", rev = "1563fe9", version = "0.23.4" }
+bevy_args = "=1.3.0"
=bevy_egui = "0.25.0"
=bevy_panorbit_camera = "0.16.0"
+clap = { version = "4.5.4", features = ["derive"] }
=derive_more = "0.99.17"
=gloo-file = "0.3.0"
=gloo-utils = "0.2.0"
@@ -19,6 +21,8 @@ rand = "0.8.5"
=regex = "1.10.3"
=ron = "0.8.1"
=serde = "1.0.197"
+serde_qs = "0.12.0"
+thiserror = "1.0.58"
=wasm-bindgen = "0.2.91"
=web-sys = {version = "0.3.69", features = ["HtmlAnchorElement"]}
=index 022a89a..9ad24e0 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -34,7 +34,7 @@ impl Plugin for BuildingsPlugin {
= .register_type::<BuildingId>()
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_buildings_systems)
- .add_systems(OnEnter(GameState::Simulate), setup_building_scenes)
+ .add_systems(OnExit(GameState::Loading), setup_building_scenes)
= .add_systems(
= Update,
= order_construction.run_if(new file mode 100644
index 0000000..3eef085
--- /dev/null
+++ b/src/configuration.rs
@@ -0,0 +1,24 @@
+use bevy::prelude::*;
+use bevy_args::{BevyArgsPlugin, Deserialize, Parser, Serialize};
+
+pub struct ConfigurationPlugin;
+
+impl Plugin for ConfigurationPlugin {
+ fn build(&self, app: &mut App) {
+ app.register_type::<Configuration>()
+ .add_plugins(BevyArgsPlugin::<Configuration>::default())
+ .add_systems(Startup, print_configuration);
+ }
+}
+
+#[derive(Resource, Clone, Debug, Default, Serialize, Deserialize, Parser, Reflect)]
+#[command(about = "Otterhide town simulator", version)]
+pub struct Configuration {
+ #[arg(long)]
+ #[serde(default)]
+ pub load: Option<String>,
+}
+
+fn print_configuration(configuration: Res<Configuration>) {
+ info!("{configuration:#?}")
+}index b660eec..581caa9 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -30,7 +30,8 @@ impl Plugin for DistrictsPlugin {
= .init_resource::<DistrictScenes>()
= .add_systems(Startup, register_district_systems)
= .add_systems(Startup, setup_assets)
- .add_systems(OnEnter(GameState::Simulate), setup_districts)
+ // FIXME: The setup_districts system runs after the first snapshot is restored!
+ .add_systems(OnExit(GameState::Loading), setup_districts)
= .add_systems(
= Update,
= plan_new_districts.after(implement_new_districts).run_if(
@@ -141,8 +142,9 @@ struct DistrictBundle {
=
=impl DistrictBundle {
= fn new(description: DistrictDescription, scenes: &DistrictScenes) -> Self {
+ let variant = &description.variant;
= let scene = SceneBundle {
- scene: scenes.0.get(&description.variant).unwrap().clone(),
+ scene: scenes.0.get(variant).unwrap().clone(),
= transform: description.borrow().into(),
= ..default()
= };
@@ -238,7 +240,7 @@ struct DistrictScenes(HashMap<DistrictVariant, Handle<Scene>>);
=
=/// This event represents a decision to construct a new building.
=///
-/// It will be stored in the history, and will result in spawning a new building.
+/// It will be stored in the history, and will result in spawning a new district.
=#[derive(Event, Clone, Debug, Serialize, Deserialize)]
=pub struct NewDistrictEstablished(pub DistrictDescription);
=
@@ -625,7 +627,7 @@ fn construct_district(
= roads_systems: Res<RoadsSystems>,
= mut commands: Commands,
=) {
- info!("Spawning a district: {description:?}");
+ info!("Spawning a district: {description:#?}");
=
= commands.run_system_with_input(
= roads_systems.implement_road_plan,index 852e363..0e943b2 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -36,7 +36,7 @@ impl Plugin for HistoryPlugin {
= }
=}
=
-#[derive(Resource, Default, Debug, Serialize, Deserialize)]
+#[derive(Resource, Default, Clone, Debug, Serialize, Deserialize)]
=pub struct History {
= pub events: Vec<EventLogEntry>,
= pub snapshots: Vec<MainSnapshot>,new file mode 100644
index 0000000..a41d203
--- /dev/null
+++ b/src/loading.rs
@@ -0,0 +1,79 @@
+use bevy::asset::AsyncReadExt;
+use bevy::{asset::AssetLoader, prelude::*};
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use crate::{configuration::Configuration, history::History, PreloadedAssets};
+
+pub struct SimulationLoadingPlugin;
+
+impl Plugin for SimulationLoadingPlugin {
+ fn build(&self, app: &mut App) {
+ app.init_asset::<Simulation>()
+ .init_asset_loader::<SimulationLoader>()
+ .add_systems(Startup, load_simulation);
+ }
+}
+
+/// 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,
+}
+
+fn load_simulation(
+ configuration: Res<Configuration>,
+ assets: Res<AssetServer>,
+ mut preloaded: ResMut<PreloadedAssets>,
+ mut commands: Commands
+) {
+ // Only load the asset if CLI / URL parameters ask for it
+ if let Some(path) = &configuration.load {
+ let handle = assets.load::<Simulation>(path);
+ preloaded.0.insert(handle.clone().untyped());
+ commands.insert_resource(SimulationAssetHandle(handle));
+ }
+}
+
+#[derive(Resource)]
+pub struct SimulationAssetHandle(pub Handle<Simulation>);
+
+
+#[derive(Default)]
+struct SimulationLoader;
+impl AssetLoader for SimulationLoader {
+ type Asset = Simulation ;
+
+ type Settings = ();
+
+ type Error = SimulationLoaderError;
+
+ fn load<'a>(
+ &'a self,
+ reader: &'a mut bevy::asset::io::Reader,
+ _settings: &'a Self::Settings,
+ _load_context: &'a mut bevy::asset::LoadContext,
+ ) -> bevy::utils::BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
+ Box::pin(async move {
+ // 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})
+ })
+ }
+
+ fn extensions(&self) -> &[&str] {
+ &["ron"] // TODO: Consider using a different extension.
+ }
+}
+#[derive(Debug, Error)]
+enum SimulationLoaderError {
+ #[error("Could not load asset: {0}")]
+ Io(#[from] std::io::Error),
+
+ #[error("Could not parse the simulation: {0}")]
+ DecodingError(#[from] ron::error::SpannedError),
+}index c32bc7f..0b5b095 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,6 @@
=mod buildings;
=mod camera;
+mod configuration;
=mod coordinates;
=mod date;
=mod day_month;
@@ -7,6 +8,7 @@ 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;
@@ -14,10 +16,11 @@ mod population;
=mod population_ui;
=mod roads;
=mod ron_export;
-mod simulation;
+mod simulation; // TODO: The simulation module holds only UI code. Merge with other UI modules.
=mod sun;
=
=use bevy::prelude::*;
+use bevy::transform::commands;
=use bevy::utils::HashSet;
=use bevy::utils::Uuid;
=use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
@@ -25,6 +28,7 @@ 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;
@@ -33,7 +37,12 @@ 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;
@@ -57,6 +66,8 @@ fn main() {
= .init_resource::<SimulationParameters>()
= .init_resource::<PreloadedAssets>()
= .init_state::<GameState>()
+ .add_plugins(ConfigurationPlugin)
+ .add_plugins(SimulationLoadingPlugin)
= .add_plugins(CameraPlugin)
= .add_plugins(GroundPlugin)
= .add_plugins(RoadsPlugin)
@@ -78,6 +89,8 @@ fn main() {
= .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
@@ -113,7 +126,7 @@ impl Default for SimulationParameters {
= sun_elevation: 600.,
= first_year: 1865,
= #[cfg(debug_assertions)]
- years: 2,
+ years: 3,
= #[cfg(not(debug_assertions))]
= years: 150,
= frame_offset: 2.0 * HOUR,
@@ -146,10 +159,16 @@ enum GameState {
=#[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();
=
@@ -157,7 +176,24 @@ fn preload_assets(
= 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!");
- state.set(GameState::Simulate);
+
+ 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)
+ }
+ }
= }
=}
=Fix "moving in" runtime error
On by
In explore mode, sometimes when the MovedIn event was preplayed a person or a building were not yet registered. That would happen if the immigration (or construction in case of buildings) happened on the same simulation frame as moving in. If the events were replayed out of order, the system would crash.
Now there are constraints ensuring that construction and immigration happen before moving in.
index 9ad24e0..6b4613b 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -42,7 +42,12 @@ impl Plugin for BuildingsPlugin {
= .and_then(resource_changed::<SimulationFrameCounter>),
= ),
= )
- .add_systems(Update, move_into_houses.run_if(on_event::<MovedIn>()))
+ .add_systems(
+ Update,
+ move_into_houses
+ .run_if(on_event::<MovedIn>())
+ .after(receive_orders),
+ )
= .add_systems(Update, receive_orders);
= }
=}index 7370353..1ab95fa 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -38,7 +38,9 @@ impl Plugin for PopulationPlugin {
= )
= .add_systems(
= Update,
- implement_immigration.run_if(on_event::<Immigrated>()),
+ implement_immigration
+ .run_if(on_event::<Immigrated>())
+ .before(search_for_houses),
= )
= .add_systems(
= Update,Implement save to a file function for desktop
On by
index 19e88b5..25a8efc 100644
--- a/src/ron_export.rs
+++ b/src/ron_export.rs
@@ -36,10 +36,7 @@ pub fn save_file(file_name: &str, content: &str) {
=/// On a desktop device, save to a file
=#[cfg(not(target_arch = "wasm32"))]
=pub fn save_file(file_name: &str, content: &str) {
- use bevy::log::error;
+ use std::fs;
=
- error!(
- "TODO: Write {length} to a {file_name}",
- length = content.len()
- );
+ fs::write(file_name, content).unwrap();
=}Setup otterhide-to-sqlite export program
On by
It doesn't do anything yet, but is there.
index 7a302c8..6ecb6d7 100644
--- a/Makefile
+++ b/Makefile
@@ -46,6 +46,11 @@ check:
= watchexec --watch src/ --debounce 1s 'cargo test && cargo clippy'
=.PHONY: check
=
+export: ## Export the simulation.ron file to SQL
+export: simulation.ron
+ cargo run --bin otterhide-to-sqlite
+.PHONY: export
+
=help: ## Print this help message
=help:
= @new file mode 100644
index 0000000..7630258
--- /dev/null
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -0,0 +1,3 @@
+fn main() {
+ println!("Hello Otterhide!")
+}POC: Generate a database using SQLx and dump it
On by
It does not use the simulation data yet and paths are hardcoded, but it does output a valid SQLite script.
index 1bc535a..254617d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,8 @@
=/web/
=/build-image
=/.cargo-home/
+/data/
=*.blend1
+
+# TODO: Do not ignore pre-recorded simulations once we got their size under control
+assets/simulation*.ronindex f1634bb..a496bcb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -72,6 +72,15 @@ dependencies = [
= "winit",
=]
=
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
=[[package]]
=name = "adler"
=version = "1.0.2"
@@ -217,6 +226,12 @@ dependencies = [
= "windows-sys 0.52.0",
=]
=
+[[package]]
+name = "anyhow"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
+
=[[package]]
=name = "approx"
=version = "0.5.1"
@@ -337,6 +352,15 @@ version = "4.7.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
=
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
=[[package]]
=name = "atomic-waker"
=version = "1.1.2"
@@ -349,12 +373,33 @@ version = "1.1.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
=
+[[package]]
+name = "backtrace"
+version = "0.3.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
=[[package]]
=name = "base64"
=version = "0.21.7"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
=
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
=[[package]]
=name = "bevy"
=version = "0.13.0"
@@ -1255,6 +1300,15 @@ version = "0.1.6"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
=
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
=[[package]]
=name = "block-sys"
=version = "0.1.0-beta.1"
@@ -1449,7 +1503,7 @@ version = "4.5.4"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
=dependencies = [
- "heck",
+ "heck 0.5.0",
= "proc-macro2",
= "quote",
= "syn 2.0.52",
@@ -1558,6 +1612,12 @@ version = "1.1.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca"
=
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
=[[package]]
=name = "const_panic"
=version = "0.2.8"
@@ -1674,6 +1734,30 @@ dependencies = [
= "windows 0.54.0",
=]
=
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
=[[package]]
=name = "crc32fast"
=version = "1.4.0"
@@ -1692,12 +1776,31 @@ dependencies = [
= "crossbeam-utils",
=]
=
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
+dependencies = [
+ "crossbeam-utils",
+]
+
=[[package]]
=name = "crossbeam-utils"
=version = "0.8.19"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
=
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
=[[package]]
=name = "cursor-icon"
=version = "1.1.0"
@@ -1727,6 +1830,17 @@ version = "2.5.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
=
+[[package]]
+name = "der"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
=[[package]]
=name = "derive_more"
=version = "0.99.17"
@@ -1740,6 +1854,18 @@ dependencies = [
= "syn 1.0.109",
=]
=
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
=[[package]]
=name = "dispatch"
=version = "0.2.0"
@@ -1755,6 +1881,12 @@ dependencies = [
= "libloading 0.8.3",
=]
=
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
=[[package]]
=name = "downcast-rs"
=version = "1.2.0"
@@ -1786,6 +1918,9 @@ name = "either"
=version = "1.10.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+dependencies = [
+ "serde",
+]
=
=[[package]]
=name = "emath"
@@ -1874,6 +2009,17 @@ version = "3.2.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
=
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
=[[package]]
=name = "euclid"
=version = "0.22.9"
@@ -1946,6 +2092,12 @@ dependencies = [
= "simd-adler32",
=]
=
+[[package]]
+name = "finl_unicode"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
+
=[[package]]
=name = "fixedbitset"
=version = "0.4.2"
@@ -1962,6 +2114,17 @@ dependencies = [
= "miniz_oxide",
=]
=
+[[package]]
+name = "flume"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin 0.9.8",
+]
+
=[[package]]
=name = "fnv"
=version = "1.0.7"
@@ -2004,12 +2167,44 @@ dependencies = [
= "percent-encoding",
=]
=
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
=[[package]]
=name = "futures-core"
=version = "0.3.30"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
=
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
=[[package]]
=name = "futures-io"
=version = "0.3.30"
@@ -2029,6 +2224,44 @@ dependencies = [
= "pin-project-lite",
=]
=
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
=[[package]]
=name = "gethostname"
=version = "0.4.3"
@@ -2086,6 +2319,12 @@ dependencies = [
= "windows 0.54.0",
=]
=
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
=[[package]]
=name = "gl_generator"
=version = "0.14.0"
@@ -2295,6 +2534,15 @@ dependencies = [
= "serde",
=]
=
+[[package]]
+name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown",
+]
+
=[[package]]
=name = "hassle-rs"
=version = "0.11.0"
@@ -2310,12 +2558,27 @@ dependencies = [
= "winapi",
=]
=
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+dependencies = [
+ "unicode-segmentation",
+]
+
=[[package]]
=name = "heck"
=version = "0.5.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
=
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
=[[package]]
=name = "hexasphere"
=version = "10.0.0"
@@ -2332,6 +2595,24 @@ version = "0.2.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
=
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
=[[package]]
=name = "home"
=version = "0.5.9"
@@ -2514,6 +2795,9 @@ name = "lazy_static"
=version = "1.4.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+dependencies = [
+ "spin 0.5.2",
+]
=
=[[package]]
=name = "lazycell"
@@ -2558,6 +2842,12 @@ dependencies = [
= "windows-targets 0.52.4",
=]
=
+[[package]]
+name = "libm"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
=[[package]]
=name = "libredox"
=version = "0.0.2"
@@ -2569,6 +2859,17 @@ dependencies = [
= "redox_syscall 0.4.1",
=]
=
+[[package]]
+name = "libsqlite3-sys"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
=[[package]]
=name = "libudev-sys"
=version = "0.1.4"
@@ -2628,6 +2929,16 @@ dependencies = [
= "regex-automata 0.1.10",
=]
=
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
=[[package]]
=name = "memchr"
=version = "2.7.1"
@@ -2674,6 +2985,17 @@ dependencies = [
= "simd-adler32",
=]
=
+[[package]]
+name = "mio"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
=[[package]]
=name = "naga"
=version = "0.19.2"
@@ -2798,6 +3120,23 @@ dependencies = [
= "winapi",
=]
=
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
=[[package]]
=name = "num-derive"
=version = "0.3.3"
@@ -2820,6 +3159,26 @@ dependencies = [
= "syn 2.0.52",
=]
=
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
=[[package]]
=name = "num-traits"
=version = "0.2.18"
@@ -2827,6 +3186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
=dependencies = [
= "autocfg",
+ "libm",
=]
=
=[[package]]
@@ -2937,6 +3297,15 @@ dependencies = [
= "objc",
=]
=
+[[package]]
+name = "object"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
+dependencies = [
+ "memchr",
+]
+
=[[package]]
=name = "oboe"
=version = "0.5.0"
@@ -3008,6 +3377,7 @@ dependencies = [
=name = "otterhide"
=version = "0.1.0"
=dependencies = [
+ "anyhow",
= "bevy",
= "bevy-inspector-egui",
= "bevy_args",
@@ -3024,7 +3394,9 @@ dependencies = [
= "ron",
= "serde",
= "serde_qs",
+ "sqlx",
= "thiserror",
+ "tokio",
= "wasm-bindgen",
= "web-sys",
=]
@@ -3079,6 +3451,15 @@ version = "1.0.14"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
=
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
=[[package]]
=name = "percent-encoding"
=version = "2.3.1"
@@ -3101,6 +3482,12 @@ version = "0.2.13"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
=
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
=[[package]]
=name = "piper"
=version = "0.2.1"
@@ -3112,6 +3499,27 @@ dependencies = [
= "futures-io",
=]
=
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
=[[package]]
=name = "pkg-config"
=version = "0.3.30"
@@ -3364,6 +3772,32 @@ dependencies = [
= "serde_derive",
=]
=
+[[package]]
+name = "rsa"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
=[[package]]
=name = "rustc-hash"
=version = "1.1.0"
@@ -3491,6 +3925,28 @@ dependencies = [
= "thiserror",
=]
=
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
=[[package]]
=name = "sharded-slab"
=version = "0.1.7"
@@ -3506,6 +3962,16 @@ version = "1.3.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
=
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
=[[package]]
=name = "simd-adler32"
=version = "0.3.7"
@@ -3573,6 +4039,31 @@ dependencies = [
= "serde",
=]
=
+[[package]]
+name = "socket2"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
=[[package]]
=name = "spirv"
=version = "0.3.0+sdk-1.3.268.0"
@@ -3582,6 +4073,220 @@ dependencies = [
= "bitflags 2.4.2",
=]
=
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlformat"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
+dependencies = [
+ "itertools",
+ "nom",
+ "unicode_categories",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
+dependencies = [
+ "ahash",
+ "atoi",
+ "byteorder",
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener 2.5.3",
+ "futures-channel",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashlink",
+ "hex",
+ "indexmap",
+ "log",
+ "memchr",
+ "once_cell",
+ "paste",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlformat",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck 0.4.1",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-sqlite",
+ "syn 1.0.109",
+ "tempfile",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags 2.4.2",
+ "byteorder",
+ "bytes",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand",
+ "rsa",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags 2.4.2",
+ "byteorder",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
+dependencies = [
+ "atoi",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "sqlx-core",
+ "tracing",
+ "url",
+ "urlencoding",
+]
+
=[[package]]
=name = "static_assertions"
=version = "1.1.0"
@@ -3594,12 +4299,29 @@ version = "0.1.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
=
+[[package]]
+name = "stringprep"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6"
+dependencies = [
+ "finl_unicode",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
=[[package]]
=name = "strsim"
=version = "0.11.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
=
+[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
=[[package]]
=name = "svg_fmt"
=version = "0.4.2"
@@ -3654,6 +4376,18 @@ dependencies = [
= "slotmap",
=]
=
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
=[[package]]
=name = "termcolor"
=version = "1.4.1"
@@ -3744,6 +4478,44 @@ version = "0.1.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
=
+[[package]]
+name = "tokio"
+version = "1.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
=[[package]]
=name = "toml_datetime"
=version = "0.6.5"
@@ -3767,6 +4539,7 @@ version = "0.1.40"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
=dependencies = [
+ "log",
= "pin-project-lite",
= "tracing-attributes",
= "tracing-core",
@@ -3860,6 +4633,12 @@ dependencies = [
= "static_assertions",
=]
=
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
=[[package]]
=name = "unicode-bidi"
=version = "0.3.15"
@@ -3899,6 +4678,12 @@ version = "0.2.4"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
=
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
=[[package]]
=name = "url"
=version = "2.5.0"
@@ -3910,6 +4695,12 @@ dependencies = [
= "percent-encoding",
=]
=
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
=[[package]]
=name = "utf8parse"
=version = "0.2.1"
@@ -3932,6 +4723,12 @@ version = "0.1.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
=
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
=[[package]]
=name = "vec_map"
=version = "0.8.2"
@@ -3960,6 +4757,12 @@ version = "0.11.0+wasi-snapshot-preview1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
=
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
=[[package]]
=name = "wasm-bindgen"
=version = "0.2.92"
@@ -4284,6 +5087,16 @@ dependencies = [
= "web-sys",
=]
=
+[[package]]
+name = "whoami"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
+dependencies = [
+ "redox_syscall 0.4.1",
+ "wasite",
+]
+
=[[package]]
=name = "widestring"
=version = "1.0.2"
@@ -4745,3 +5558,9 @@ dependencies = [
= "quote",
= "syn 2.0.52",
=]
+
+[[package]]
+name = "zeroize"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"index 0005c94..c9ad9af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
=# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
=
=[dependencies]
+anyhow = "1.0.81"
=bevy = { version = "0.13", features = ["wayland", "serialize"] }
=bevy-inspector-egui = { git = "https://github.com/jakobhellermann/bevy-inspector-egui", rev = "1563fe9", version = "0.23.4" }
=bevy_args = "=1.3.0"
@@ -22,7 +23,9 @@ 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"] }
=thiserror = "1.0.58"
+tokio = { version = "1.37.0", features = ["rt", "macros"] }
=wasm-bindgen = "0.2.91"
=web-sys = {version = "0.3.69", features = ["HtmlAnchorElement"]}
=index 6ecb6d7..8d50add 100644
--- a/Makefile
+++ b/Makefile
@@ -46,10 +46,16 @@ check:
= watchexec --watch src/ --debounce 1s 'cargo test && cargo clippy'
=.PHONY: check
=
-export: ## Export the simulation.ron file to SQL
-export: simulation.ron
- cargo run --bin otterhide-to-sqlite
-.PHONY: export
+sqlit-export: ## Export the simulation.ron file to SQL
+sqlit-export: data/sqlite-export.sql
+.PHONY: sqlit-export
+
+data/sqlite-export.sql: data/sqlite-export.db
+ sqlite3 $< .dump > $@
+
+data/sqlite-export.db: assets/simulation.ron
+ cargo run --bin otterhide-to-sqlite $< $@
+
=
=help: ## Print this help message
=help:index 7630258..fbb3bb7 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -1,3 +1,46 @@
-fn main() {
- println!("Hello Otterhide!")
+use anyhow::Context;
+use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> anyhow::Result<()> {
+ // TODO: Get the input filename from command line
+ // TODO: Get the output filename from command line
+ let db_filename = std::path::Path::new("data/sqlite-export.db");
+
+ if db_filename.exists() {
+ std::fs::remove_file(db_filename).context("Removing previous database")?;
+ }
+
+ let connect_options = SqliteConnectOptions::new()
+ .create_if_missing(true)
+ .filename(db_filename);
+ 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")
+ .execute(&mut *connection)
+ .await
+ .context("Dropping districts table")?;
+
+ sqlx::query(
+ "Create table districts (
+ name text
+ )",
+ )
+ .execute(&mut *connection)
+ .await
+ .context("Creating districts table")?;
+
+ sqlx::query("Insert into districts (name) values (?)")
+ .bind("Blueberry Hills")
+ .execute(&mut *connection)
+ .await
+ .context("Inserting a district")?;
+
+ connection.close().await.context("Closing the connection")
=}Remove unused import
On by
Added by mistake.
index 0b5b095..cba3882 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -20,7 +20,6 @@ mod simulation; // TODO: The simulation module holds only UI code. Merge with ot
=mod sun;
=
=use bevy::prelude::*;
-use bevy::transform::commands;
=use bevy::utils::HashSet;
=use bevy::utils::Uuid;
=use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;SQLite export will accept input and output arguments
On by
The output is respected. Input is ignored for now.
index fbb3bb7..130ff69 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -1,19 +1,28 @@
=use anyhow::Context;
+use clap::Parser;
=use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
+use std::path::PathBuf;
+
+#[derive(Parser)]
+#[command(version, about, long_about = None)]
+struct CLI {
+ /// Input .ron file
+ simulation: PathBuf,
+ /// Output .db file
+ database: PathBuf,
+}
=
=#[tokio::main(flavor = "current_thread")]
=async fn main() -> anyhow::Result<()> {
- // TODO: Get the input filename from command line
- // TODO: Get the output filename from command line
- let db_filename = std::path::Path::new("data/sqlite-export.db");
+ let cli = CLI::parse();
=
- if db_filename.exists() {
- std::fs::remove_file(db_filename).context("Removing previous database")?;
+ if cli.database.exists() {
+ std::fs::remove_file(&cli.database).context("Removing previous database")?;
= }
=
= let connect_options = SqliteConnectOptions::new()
= .create_if_missing(true)
- .filename(db_filename);
+ .filename(&cli.database);
= let pool = SqlitePool::connect_with(connect_options)
= .await
= .context("Connecting to the database")?;