From 50dd3bde9c372d325ca37f03266082fcb196558d Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Thu, 13 Jan 2022 13:44:54 +0200 Subject: [PATCH 01/46] Demote frame allocator logging to trace --- game/simulation/src/alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/simulation/src/alloc.rs b/game/simulation/src/alloc.rs index 54006e60..24e0033b 100644 --- a/game/simulation/src/alloc.rs +++ b/game/simulation/src/alloc.rs @@ -7,7 +7,7 @@ impl FrameAllocator { pub fn reset(&mut self) { let bytes = self.0.allocated_bytes(); if bytes > 0 { - debug!("freeing {} bytes in frame allocator", bytes); + trace!("freeing {} bytes in frame allocator", bytes); } self.0.reset(); From 65b25e07ff58408f7a45c6ea6f00a265f18710e1 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Thu, 13 Jan 2022 14:29:51 +0200 Subject: [PATCH 02/46] Fix common material over-reservation case --- .planning/active.md | 8 +++ TODOS.md | 5 +- game/ai/src/decision.rs | 16 ++++-- game/ai/src/intelligence.rs | 2 +- game/simulation/src/ai/dse/world.rs | 9 +++- game/simulation/src/ecs/world_ext.rs | 49 +++++++++++++---- game/simulation/src/scripting/nop.rs | 6 +-- game/simulation/src/society/job/jobs/build.rs | 53 ++++++++++++++++--- game/simulation/src/society/job/jobs/mod.rs | 2 +- renderer/main/src/scenarios.rs | 1 + 10 files changed, 118 insertions(+), 33 deletions(-) create mode 100644 .planning/active.md diff --git a/.planning/active.md b/.planning/active.md new file mode 100644 index 00000000..68860c03 --- /dev/null +++ b/.planning/active.md @@ -0,0 +1,8 @@ +# Active tasks + +* [o] fix over reserving of materials +* [ ] pre-filled society stores of bricks +* [ ] ui for creating jobs for building many blocks +* [ ] ui for wall outline specifically +* [ ] extend material reservation to include the specific materials in transit for a build, + to avoid others considering hauling more when it's already on the way diff --git a/TODOS.md b/TODOS.md index cb837a35..ec3069f6 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,4 +1,4 @@ -# TODOs (401) +# TODOs (400) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) @@ -332,12 +332,11 @@ * `/// TODO provide size hint that could be used as an optimisation for a small number of tasks (e.g. smallvec)` * [game/simulation/src/society/job/jobs/break_blocks.rs](game/simulation/src/society/job/jobs/break_blocks.rs) (1) * `// TODO add display impl for WorldPositionRange` - * [game/simulation/src/society/job/jobs/build.rs](game/simulation/src/society/job/jobs/build.rs) (11) + * [game/simulation/src/society/job/jobs/build.rs](game/simulation/src/society/job/jobs/build.rs) (10) * `// TODO build requirement engine for generic material combining` * `// TODO support builds spanning multiple blocks/range` * `// TODO fewer temporary allocations` * `// TODO ensure this doesn't happen, or just handle it properly` - * `// TODO avoid this case` * `// TODO allow "building" of a non-air block, and automatically emit a break task first?` * `// TODO job is destined to fail...` * `// TODO dont run this every tick, only when something changes or intermittently` diff --git a/game/ai/src/decision.rs b/game/ai/src/decision.rs index 98727d10..f19fcd46 100644 --- a/game/ai/src/decision.rs +++ b/game/ai/src/decision.rs @@ -40,6 +40,10 @@ pub trait Dse { name.strip_suffix("Dse").unwrap_or(name) } + fn as_debug(&self) -> Option<&dyn Debug> { + None + } + fn weight(&self) -> DecisionWeight { DecisionWeight::Plain(self.weight_type()) } @@ -192,10 +196,6 @@ impl> WeightedDse { } impl> Dse for WeightedDse { - fn name(&self) -> &'static str { - self.dse.name() - } - fn considerations(&self, out: &mut Considerations) { self.dse.considerations(out) } @@ -208,6 +208,14 @@ impl> Dse for WeightedDse { self.dse.action(blackboard) } + fn name(&self) -> &'static str { + self.dse.name() + } + + fn as_debug(&self) -> Option<&dyn Debug> { + self.dse.as_debug() + } + fn weight(&self) -> DecisionWeight { DecisionWeight::Weighted(self.dse.weight_type(), self.additional_weight) } diff --git a/game/ai/src/intelligence.rs b/game/ai/src/intelligence.rs index db02e2a7..f5f83f9f 100644 --- a/game/ai/src/intelligence.rs +++ b/game/ai/src/intelligence.rs @@ -177,7 +177,7 @@ impl Intelligence { .unwrap() // not empty }; - trace!("intelligence chose {dse}", dse = choice.name(); "source" => ?choice_src); + trace!("intelligence chose {dse}", dse = choice.name(); "source" => ?choice_src, "detail" => ?choice.as_debug()); drop(context); let action = choice.action(blackboard); diff --git a/game/simulation/src/ai/dse/world.rs b/game/simulation/src/ai/dse/world.rs index 338decb6..d9eed6bf 100644 --- a/game/simulation/src/ai/dse/world.rs +++ b/game/simulation/src/ai/dse/world.rs @@ -3,6 +3,7 @@ use crate::ai::consideration::{ BlockTypeMatchesConsideration, FindLocalGradedItemConsideration, HasExtraHandsForHaulingConsideration, MyProximityToConsideration, Proximity, }; +use std::fmt::Debug; use crate::ai::input::BlockTypeMatch; use crate::ai::{AiAction, AiContext}; @@ -24,6 +25,7 @@ pub struct BuildDse { pub details: BuildDetails, } +#[derive(Debug)] pub struct GatherMaterialsDse { pub target: WorldPosition, pub material: BuildMaterial, @@ -101,6 +103,10 @@ impl Dse for GatherMaterialsDse { todo!("gather materials from a different source") } + + fn as_debug(&self) -> Option<&dyn Debug> { + Some(self) + } } impl GatherMaterialsDse { @@ -111,7 +117,8 @@ impl GatherMaterialsDse { let filter = self.filter(); if let AiAction::Haul(haulee, source, ..) = blackboard.ai.last_action() { if (*haulee, Some(blackboard.world)).matches(filter) { - // use this thing in inventory for hauling + // use this thing in inventory for hauling. even if the stack is too big, it will be + // split when it arrives at the build site return Some((*haulee, *source)); } } diff --git a/game/simulation/src/ecs/world_ext.rs b/game/simulation/src/ecs/world_ext.rs index 3a84a3de..e711872b 100644 --- a/game/simulation/src/ecs/world_ext.rs +++ b/game/simulation/src/ecs/world_ext.rs @@ -2,11 +2,11 @@ use unit::world::WorldPoint; use crate::build::{ConsumedMaterialForJobComponent, ReservedMaterialComponent}; use crate::ecs::{EcsWorld, Entity, WorldExt}; -use crate::{ComponentWorld, TransformComponent}; +use crate::{ComponentWorld, ContainersError, TransformComponent}; use common::*; use crate::item::{ContainedInComponent, EndHaulBehaviour, HaulType, HauledItemComponent}; -use crate::job::{BuildThingJob, SocietyJobHandle}; +use crate::job::{BuildThingError, BuildThingJob, SocietyJobHandle}; #[derive(common::derive_more::Deref, common::derive_more::DerefMut)] pub struct EcsExtComponents<'w>(&'w EcsWorld); @@ -108,15 +108,39 @@ impl EcsExtComponents<'_> { material: Entity, job: SocietyJobHandle, ) -> Result<(), ReservationError> { - // find job in society - let succeeded = job + // find job in society and try to reserve + let surplus = job .resolve_and_cast_mut(self.0.resource(), |build_job: &mut BuildThingJob| { - build_job.add_reservation(material) + build_job.add_reservation(material, self.0) }) - .ok_or(ReservationError::InvalidJob(job))?; - - if !succeeded { - return Err(ReservationError::AlreadyReserved(job, material)); + .ok_or(ReservationError::InvalidJob(job))??; + + if let Some(surplus) = surplus { + // drop surplus + debug!("splitting {n} surplus materials from build reservation", n = surplus; "job" => ?job); + let new_stack = self + .0 + .helpers_containers() + .split_stack(material, surplus) + .map_err(ReservationError::DropSurplus)?; + + // drop it at a slight offset + if new_stack != material { + let mut transform = self + .0 + .component_mut::(new_stack) + .expect("transform expected"); + + let pos = { + let mut rand = thread_rng(); + let mut pos = transform.position; + pos.modify_x(|x| x + rand.gen_range(-1.0, 1.0)); + pos.modify_y(|y| y + rand.gen_range(-1.0, 1.0)); + pos + }; + + transform.reset_position(pos) + } } // no more failures, now we can add the reservation to the item @@ -157,6 +181,9 @@ pub enum ReservationError { #[error("Job used for society material reservation is not a build job: {0:?}")] InvalidJob(SocietyJobHandle), - #[error("Material {1} is already reserved for build job {0:?}")] - AlreadyReserved(SocietyJobHandle, Entity), + #[error("Failed to reserve material: {0}")] + BuildJob(#[from] BuildThingError), + + #[error("Failed to drop surplus materials: {0}")] + DropSurplus(ContainersError), } diff --git a/game/simulation/src/scripting/nop.rs b/game/simulation/src/scripting/nop.rs index bb560b0b..14bf1f6a 100644 --- a/game/simulation/src/scripting/nop.rs +++ b/game/simulation/src/scripting/nop.rs @@ -8,11 +8,7 @@ impl Scripting for NopScripting { Ok(NopScripting) } - fn run( - &mut self, - _script: &[u8], - _ecs: &EcsWorld, - ) -> ScriptingResult { + fn run(&mut self, _script: &[u8], _ecs: &EcsWorld) -> ScriptingResult { Ok(ScriptingOutput::default()) } } diff --git a/game/simulation/src/society/job/jobs/build.rs b/game/simulation/src/society/job/jobs/build.rs index 9bd868a1..4d2982ee 100644 --- a/game/simulation/src/society/job/jobs/build.rs +++ b/game/simulation/src/society/job/jobs/build.rs @@ -67,6 +67,12 @@ pub struct BuildProgressDetails { pub enum BuildThingError { #[error("Build materials are invalid")] InvalidMaterials, + + #[error("No definition for material {0}")] + MissingDefinition(Entity), + + #[error("Material '{0}' is not required")] + MaterialNotRequired(String), } impl BuildThingJob { @@ -151,12 +157,10 @@ impl BuildThingJob { let quantity = stack_opt.map(|comp| comp.stack.total_count()).unwrap_or(1); *entry = entry.checked_sub(quantity).unwrap_or_else(|| { - warn!( + unreachable!( "tried to over-reserve {}/{} of remaining requirement '{}'", quantity, *entry, def.0 - ); - // TODO avoid this case - 0 + ) }); } @@ -167,9 +171,44 @@ impl BuildThingJob { .collect() } - /// Returns false if already reserved. Entity should have ReservedMaterialComponent - pub fn add_reservation(&mut self, reservee: Entity) -> bool { - self.reserved_materials.insert(reservee) + /// Returns Ok(Some(surplus)) if there is a surplus of material to drop in a new stack. + /// Entity should have ReservedMaterialComponent on success + pub fn add_reservation( + &mut self, + reservee: Entity, + world: &EcsWorld, + ) -> Result, BuildThingError> { + let stacks = world.read_storage::(); + let defs = world.read_storage::(); + + let n_required = { + let def = reservee + .get(&defs) + .ok_or(BuildThingError::MissingDefinition(reservee))?; + + match self.materials_remaining.get(def.0.as_str()) { + None => return Err(BuildThingError::MaterialNotRequired(def.0.clone())), + Some(n) => n.get(), + } + }; + + let n_actual = { + reservee + .get(&stacks) + .map(|stack| stack.stack.total_count()) + .unwrap_or(1) + }; + + let result = if n_actual > n_required { + // trying to reserve too many, keep the original stack but split off some + let to_drop = n_actual - n_required; + Some(to_drop) + } else { + None + }; + + let _ = self.reserved_materials.insert(reservee); + Ok(result) } pub fn reserved_materials(&self) -> impl Iterator + '_ { diff --git a/game/simulation/src/society/job/jobs/mod.rs b/game/simulation/src/society/job/jobs/mod.rs index b918e417..f9c4336e 100644 --- a/game/simulation/src/society/job/jobs/mod.rs +++ b/game/simulation/src/society/job/jobs/mod.rs @@ -1,5 +1,5 @@ pub use break_blocks::BreakBlocksJob; -pub use build::{BuildDetails, BuildProgressDetails, BuildThingJob}; +pub use build::{BuildDetails, BuildProgressDetails, BuildThingError, BuildThingJob}; pub use haul::HaulJob; mod break_blocks; diff --git a/renderer/main/src/scenarios.rs b/renderer/main/src/scenarios.rs index 85c19af8..f678e053 100644 --- a/renderer/main/src/scenarios.rs +++ b/renderer/main/src/scenarios.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] use crate::scenarios::helpers::{spawn_entities_randomly, Placement}; +use crate::simulation::WorldPosition; use common::*; use engine::simulation; use simulation::job::BuildThingJob; From 38320203fed7fe49e14a1c6f85f8fc0240d49b80 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Fri, 14 Jan 2022 11:31:25 +0200 Subject: [PATCH 03/46] Fix material over-reservations for realsies --- .planning/active.md | 2 +- .../src/activity/subactivity/haul.rs | 1 - game/simulation/src/ecs/world_ext.rs | 58 +++++++++-------- game/simulation/src/simulation.rs | 3 +- game/simulation/src/society/job/jobs/build.rs | 64 +++++++++++++------ game/simulation/src/society/job/jobs/mod.rs | 4 +- 6 files changed, 82 insertions(+), 50 deletions(-) diff --git a/.planning/active.md b/.planning/active.md index 68860c03..ca8e8bb8 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -1,6 +1,6 @@ # Active tasks -* [o] fix over reserving of materials +* [X] fix over reserving of materials * [ ] pre-filled society stores of bricks * [ ] ui for creating jobs for building many blocks * [ ] ui for wall outline specifically diff --git a/game/simulation/src/activity/subactivity/haul.rs b/game/simulation/src/activity/subactivity/haul.rs index 444308bc..31ec3840 100644 --- a/game/simulation/src/activity/subactivity/haul.rs +++ b/game/simulation/src/activity/subactivity/haul.rs @@ -224,7 +224,6 @@ impl<'a> HaulSubactivity<'a> { self.ctx.world().resource::().queue( "reserve material", move |world| { - trace!("reserving material for job"; "material" => thing, "job" => ?job); world.helpers_comps().reserve_material_for_job(thing, job)?; Ok(()) }, diff --git a/game/simulation/src/ecs/world_ext.rs b/game/simulation/src/ecs/world_ext.rs index e711872b..f0e0dc20 100644 --- a/game/simulation/src/ecs/world_ext.rs +++ b/game/simulation/src/ecs/world_ext.rs @@ -6,7 +6,7 @@ use crate::{ComponentWorld, ContainersError, TransformComponent}; use common::*; use crate::item::{ContainedInComponent, EndHaulBehaviour, HaulType, HauledItemComponent}; -use crate::job::{BuildThingError, BuildThingJob, SocietyJobHandle}; +use crate::job::{BuildThingError, BuildThingJob, MaterialReservation, SocietyJobHandle}; #[derive(common::derive_more::Deref, common::derive_more::DerefMut)] pub struct EcsExtComponents<'w>(&'w EcsWorld); @@ -109,37 +109,43 @@ impl EcsExtComponents<'_> { job: SocietyJobHandle, ) -> Result<(), ReservationError> { // find job in society and try to reserve - let surplus = job + let (surplus, remaining) = job .resolve_and_cast_mut(self.0.resource(), |build_job: &mut BuildThingJob| { build_job.add_reservation(material, self.0) }) .ok_or(ReservationError::InvalidJob(job))??; - if let Some(surplus) = surplus { - // drop surplus - debug!("splitting {n} surplus materials from build reservation", n = surplus; "job" => ?job); - let new_stack = self - .0 - .helpers_containers() - .split_stack(material, surplus) - .map_err(ReservationError::DropSurplus)?; - - // drop it at a slight offset - if new_stack != material { - let mut transform = self + match surplus { + MaterialReservation::ConsumeAll(n) => { + debug!("reserving material for build job"; "material" => material, "job" => ?job, "n" => n, "remaining" => remaining); + } + MaterialReservation::Surplus { surplus, reserved } => { + // drop surplus + debug!("reserving {n} material for build job with {surplus} surplus to be split into new stack", + n = reserved, surplus = surplus; "job" => ?job); + let new_stack = self .0 - .component_mut::(new_stack) - .expect("transform expected"); - - let pos = { - let mut rand = thread_rng(); - let mut pos = transform.position; - pos.modify_x(|x| x + rand.gen_range(-1.0, 1.0)); - pos.modify_y(|y| y + rand.gen_range(-1.0, 1.0)); - pos - }; - - transform.reset_position(pos) + .helpers_containers() + .split_stack(material, surplus) + .map_err(ReservationError::DropSurplus)?; + + // drop it at a slight offset + if new_stack != material { + let mut transform = self + .0 + .component_mut::(new_stack) + .expect("transform expected"); + + let pos = { + let mut rand = thread_rng(); + let mut pos = transform.position; + pos.modify_x(|x| x + rand.gen_range(-1.0, 1.0)); + pos.modify_y(|y| y + rand.gen_range(-1.0, 1.0)); + pos + }; + + transform.reset_position(pos) + } } } diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index d433f5cc..d993fb3e 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -31,7 +31,7 @@ use crate::render::{RenderSystem, Renderer}; use crate::senses::{SensesDebugRenderer, SensesSystem}; use crate::alloc::FrameAllocator; -use crate::job::SocietyJobHandle; + use crate::runtime::{Runtime, RuntimeSystem}; use crate::scripting::ScriptingContext; use crate::society::{NameGeneration, PlayerSociety}; @@ -45,7 +45,6 @@ use crate::{ use crate::{ComponentWorld, Societies, SocietyHandle}; use std::collections::HashSet; use std::pin::Pin; -use std::sync::Arc; #[derive(Debug, EnumDiscriminants)] #[strum_discriminants(name(AssociatedBlockDataType))] diff --git a/game/simulation/src/society/job/jobs/build.rs b/game/simulation/src/society/job/jobs/build.rs index 4d2982ee..d49e2e85 100644 --- a/game/simulation/src/society/job/jobs/build.rs +++ b/game/simulation/src/society/job/jobs/build.rs @@ -75,6 +75,13 @@ pub enum BuildThingError { MaterialNotRequired(String), } +pub enum MaterialReservation { + /// Whole stack of this size is reserved + ConsumeAll(u16), + /// Stack has too many materials, should be split from original stack + Surplus { surplus: u16, reserved: u16 }, +} + impl BuildThingJob { pub fn new(block: WorldPosition, build: impl Build + 'static) -> Self { let mut materials = Vec::new(); @@ -149,7 +156,7 @@ impl BuildThingJob { let def_names = world.read_storage::(); let stacks = world.read_storage::(); - for (_, def, stack_opt) in (&reservations_bitset, &def_names, stacks.maybe()).join() { + for (e, def, stack_opt) in (&reservations_bitset, &def_names, stacks.maybe()).join() { let entry = remaining_materials .get_mut(def.0.as_str()) .unwrap_or_else(|| panic!("invalid reservation {:?}", def.0)); @@ -158,8 +165,8 @@ impl BuildThingJob { let quantity = stack_opt.map(|comp| comp.stack.total_count()).unwrap_or(1); *entry = entry.checked_sub(quantity).unwrap_or_else(|| { unreachable!( - "tried to over-reserve {}/{} of remaining requirement '{}'", - quantity, *entry, def.0 + "tried to over-reserve {}/{} of remaining requirement '{}' in job {:?} (material = {})", + quantity, *entry, def.0, this_job, e ) }); } @@ -171,26 +178,26 @@ impl BuildThingJob { .collect() } - /// Returns Ok(Some(surplus)) if there is a surplus of material to drop in a new stack. - /// Entity should have ReservedMaterialComponent on success + /// Returns (Surplus(n), _) if there is a surplus of material to drop in a NEW stack. + /// Second return val is the now-remaining requirement for this material. + /// Entity should get a ReservedMaterialComponent on success pub fn add_reservation( &mut self, reservee: Entity, world: &EcsWorld, - ) -> Result, BuildThingError> { + ) -> Result<(MaterialReservation, u16), BuildThingError> { let stacks = world.read_storage::(); let defs = world.read_storage::(); - let n_required = { - let def = reservee - .get(&defs) - .ok_or(BuildThingError::MissingDefinition(reservee))?; + let def = reservee + .get(&defs) + .ok_or(BuildThingError::MissingDefinition(reservee))?; - match self.materials_remaining.get(def.0.as_str()) { - None => return Err(BuildThingError::MaterialNotRequired(def.0.clone())), - Some(n) => n.get(), - } - }; + let n_required_ref = self + .materials_remaining + .get_mut(def.0.as_str()) + .ok_or_else(|| BuildThingError::MaterialNotRequired(def.0.clone()))?; + let n_required = n_required_ref.get(); let n_actual = { reservee @@ -199,16 +206,36 @@ impl BuildThingJob { .unwrap_or(1) }; + let n_to_reserve; let result = if n_actual > n_required { // trying to reserve too many, keep the original stack but split off some let to_drop = n_actual - n_required; - Some(to_drop) + n_to_reserve = n_required; + MaterialReservation::Surplus { + surplus: to_drop, + reserved: n_required, + } } else { - None + n_to_reserve = n_actual; + MaterialReservation::ConsumeAll(n_actual) }; + // reserve material entity let _ = self.reserved_materials.insert(reservee); - Ok(result) + + // reduce requirement count + let remaining = match NonZeroU16::new(n_required - n_to_reserve) { + Some(n) => { + *n_required_ref = n; + n.get() + } + None => { + self.materials_remaining.remove(def.0.as_str()); + 0 + } + }; + + Ok((result, remaining)) } pub fn reserved_materials(&self) -> impl Iterator + '_ { @@ -347,7 +374,6 @@ impl SocietyJobImpl for BuildThingJob { // all gather requirements are satisfied, do the build // TODO some builds could have multiple workers - let this_job = self.this_job.unwrap(); // set unconditionally tasks.push(SocietyTask::Build(this_job, self.details())); } diff --git a/game/simulation/src/society/job/jobs/mod.rs b/game/simulation/src/society/job/jobs/mod.rs index f9c4336e..3c3cf8c5 100644 --- a/game/simulation/src/society/job/jobs/mod.rs +++ b/game/simulation/src/society/job/jobs/mod.rs @@ -1,5 +1,7 @@ pub use break_blocks::BreakBlocksJob; -pub use build::{BuildDetails, BuildProgressDetails, BuildThingError, BuildThingJob}; +pub use build::{ + BuildDetails, BuildProgressDetails, BuildThingError, BuildThingJob, MaterialReservation, +}; pub use haul::HaulJob; mod break_blocks; From de024532005d8b738b6aaf0532a61cf6c0db72bb Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Mon, 17 Jan 2022 10:09:49 +0200 Subject: [PATCH 04/46] Add realtime selection rendering --- .planning/active.md | 9 +- game/simulation/src/backend.rs | 8 +- game/simulation/src/input/event.rs | 15 +- game/simulation/src/input/mod.rs | 2 +- game/simulation/src/input/system.rs | 141 ++++++++++++------ game/simulation/src/render/system.rs | 11 +- game/simulation/src/simulation.rs | 11 +- renderer/engine/src/render/sdl/backend.rs | 9 +- renderer/engine/src/render/sdl/selection.rs | 65 ++++---- .../src/render/sdl/ui/windows/selection.rs | 21 ++- .../src/render/sdl/ui/windows/society.rs | 2 +- shared/unit/src/world/world_point.rs | 31 ++++ 12 files changed, 225 insertions(+), 100 deletions(-) diff --git a/.planning/active.md b/.planning/active.md index ca8e8bb8..9b1a80f2 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -2,7 +2,14 @@ * [X] fix over reserving of materials * [ ] pre-filled society stores of bricks +* [X] show ui block selection as it is dragged + * [ ] block selection can be added to with ctrl+drag + * [ ] show selection dimensions in world + * [ ] use left click for all selections? * [ ] ui for creating jobs for building many blocks -* [ ] ui for wall outline specifically +* [ ] ui for wall outline specifically? + * hovering over button should show outline preview + * [ ] can specify thickness of wall + * [ ] can shrink/expand selection by 1 block * [ ] extend material reservation to include the specific materials in transit for a build, to avoid others considering hauling more when it's already on the way diff --git a/game/simulation/src/backend.rs b/game/simulation/src/backend.rs index 44dc6c54..eec0ce68 100644 --- a/game/simulation/src/backend.rs +++ b/game/simulation/src/backend.rs @@ -1,9 +1,9 @@ use crate::input::UiCommands; use crate::perf::PerfAvg; use crate::{Renderer, Simulation, WorldViewer}; -use common::{Error, NotNan}; +use common::Error; use resources::Resources; -use unit::world::WorldPosition; +use unit::world::{WorldPoint2d, WorldPosition}; #[derive(Debug)] pub enum Exit { @@ -22,8 +22,8 @@ pub enum Exit { /// Populated by backend events #[derive(Default)] pub struct BackendData { - /// Mouse position in WORLD SPACE - pub mouse_position: Option<(NotNan, NotNan)>, + /// Mouse position in world space + pub mouse_position: Option, } pub trait InitializedSimulationBackend: Sized { diff --git a/game/simulation/src/input/event.rs b/game/simulation/src/input/event.rs index bf7ad1fe..b298dd9e 100644 --- a/game/simulation/src/input/event.rs +++ b/game/simulation/src/input/event.rs @@ -15,8 +15,19 @@ pub struct WorldColumn { #[derive(Debug)] pub enum InputEvent { Click(SelectType, WorldColumn), - /// (_, from ,to) - Select(SelectType, WorldColumn, WorldColumn), + /// Selection has been made + Select { + select: SelectType, + from: WorldColumn, + to: WorldColumn, + progress: SelectionProgress, + }, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum SelectionProgress { + InProgress, + Complete, } /// Resource for current mouse location in WORLD SPACE diff --git a/game/simulation/src/input/mod.rs b/game/simulation/src/input/mod.rs index 2efe3417..2838cbf5 100644 --- a/game/simulation/src/input/mod.rs +++ b/game/simulation/src/input/mod.rs @@ -3,5 +3,5 @@ mod event; mod system; pub use command::*; -pub use event::{InputEvent, MouseLocation, SelectType, WorldColumn}; +pub use event::{InputEvent, MouseLocation, SelectType, SelectionProgress, WorldColumn}; pub use system::{InputSystem, SelectedComponent, SelectedEntity, SelectedTiles}; diff --git a/game/simulation/src/input/system.rs b/game/simulation/src/input/system.rs index aa2779a7..2eb47fe2 100644 --- a/game/simulation/src/input/system.rs +++ b/game/simulation/src/input/system.rs @@ -2,7 +2,7 @@ use common::*; use unit::world::{WorldPosition, WorldPositionRange, WorldRange}; use crate::ecs::*; -use crate::input::{InputEvent, SelectType, WorldColumn}; +use crate::input::{InputEvent, SelectType, SelectionProgress, WorldColumn}; use crate::spatial::{Spatial, Transforms}; use crate::TransformComponent; use crate::{UiElementComponent, WorldRef}; @@ -24,7 +24,10 @@ pub struct SelectedComponent; pub struct SelectedEntity(Option); #[derive(Default, Clone)] -pub struct SelectedTiles(Option); +pub struct SelectedTiles { + current: Option<(WorldPositionRange, SelectionProgress)>, + last: Option<(WorldPosition, WorldPosition, SelectionProgress)>, +} const TILE_SELECTION_LIMIT: f32 = 50.0; @@ -84,54 +87,26 @@ impl<'a> System<'a> for InputSystem<'a> { } } - InputEvent::Select(SelectType::Left, _, _) => { + InputEvent::Select { + select: SelectType::Left, + .. + } => { // TODO select multiple entities } InputEvent::Click(SelectType::Right, _) => { // unselect tile selection - selected_block.0 = None; + selected_block.clear(); } - InputEvent::Select(SelectType::Right, from, to) => { - // select tiles - let w = (*world).borrow(); - - // limit selection size by moving the second point placed. this isn't totally - // accurate and may allow selections of 1 block bigger, but meh who cares - let to = { - let dx = (from.x - to.x).abs(); - let dy = (from.y - to.y).abs(); - - let x_overlap = TILE_SELECTION_LIMIT - dx; - let y_overlap = TILE_SELECTION_LIMIT - dy; - - let mut to = *to; - if x_overlap < 0.0 { - let mul = if from.x > to.x { 1.0 } else { -1.0 }; - to.x -= mul * x_overlap; - } - - if y_overlap < 0.0 { - let mul = if from.y > to.y { 1.0 } else { -1.0 }; - to.y -= mul * y_overlap; - } - - to - }; - - selected_block.0 = from.find_min_max_walkable(&to, &w).map(|(min, max)| { - let mut a = min.floor(); - let mut b = max.floor(); - - // these blocks are walkable air blocks, move them down 1 to select the - // actual block beneath - a.2 -= 1; - b.2 -= 1; - - debug!("selecting tiles"; "min" => %a, "max" => %b); - WorldPositionRange::with_inclusive_range(a, b) - }); + InputEvent::Select { + select: SelectType::Right, + from, + to, + progress, + } => { + // update tile selection + selected_block.update((*from, *to), *progress, &world); } } } @@ -184,15 +159,85 @@ impl SelectedEntity { } impl SelectedTiles { - pub fn range(&self) -> Option { - self.0.as_ref().cloned() + fn update( + &mut self, + range: (WorldColumn, WorldColumn), + progress: SelectionProgress, + world: &WorldRef, + ) { + let (from, mut to) = range; + + // limit selection size by moving the second point placed. this isn't totally + // accurate and may allow selections of 1 block bigger, but meh who cares + let to = { + let dx = (from.x - to.x).abs(); + let dy = (from.y - to.y).abs(); + + let x_overlap = TILE_SELECTION_LIMIT - dx; + let y_overlap = TILE_SELECTION_LIMIT - dy; + + if x_overlap < 0.0 { + let mul = if from.x > to.x { 1.0 } else { -1.0 }; + to.x -= mul * x_overlap; + } + + if y_overlap < 0.0 { + let mul = if from.y > to.y { 1.0 } else { -1.0 }; + to.y -= mul * y_overlap; + } + + to + }; + + let w = world.borrow(); + if let Some((min, max)) = from.find_min_max_walkable(&to, &w) { + let mut a = min.floor(); + let mut b = max.floor(); + + // these blocks are walkable air blocks, move them down 1 to select the + // actual block beneath + a.2 -= 1; + b.2 -= 1; + + if Some((a, b, progress)) != self.last { + self.last = Some((a, b, progress)); + self.current = Some((WorldPositionRange::with_inclusive_range(a, b), progress)); + + if let SelectionProgress::Complete = progress { + debug!("selecting tiles"; "min" => %a, "max" => %b); + } + } + } + } + + fn clear(&mut self) { + self.current = None; + } + + /// Includes in-progress selection + pub fn bounds(&self) -> Option<(SelectionProgress, (WorldPosition, WorldPosition))> { + self.current + .as_ref() + .map(|(range, progress)| (*progress, range.bounds())) } - pub fn bounds(&self) -> Option<(WorldPosition, WorldPosition)> { - self.0.as_ref().map(|range| range.bounds()) + + fn selected(&self) -> Option<&WorldPositionRange> { + match self.current.as_ref() { + Some((range, SelectionProgress::Complete)) => Some(range), + _ => None, + } + } + + pub fn selected_range(&self) -> Option { + self.selected().cloned() + } + + pub fn selected_bounds(&self) -> Option<(WorldPosition, WorldPosition)> { + self.selected().map(|range| range.bounds()) } pub fn single_tile(&self) -> Option { - self.0.clone().and_then(|range| match range { + self.selected().and_then(|range| match *range { WorldRange::Single(pos) => Some(pos), WorldRange::Range(a, b) if a == b => Some(a), _ => None, diff --git a/game/simulation/src/render/system.rs b/game/simulation/src/render/system.rs index 18c0f079..fdfb3f00 100644 --- a/game/simulation/src/render/system.rs +++ b/game/simulation/src/render/system.rs @@ -7,7 +7,7 @@ use common::*; use unit::world::BLOCKS_PER_METRE; use crate::ecs::*; -use crate::input::{SelectedComponent, SelectedTiles}; +use crate::input::{SelectedComponent, SelectedTiles, SelectionProgress}; use crate::render::renderer::Renderer; use crate::render::shape::RenderHexColor; @@ -105,9 +105,12 @@ impl<'a, R: Renderer> System<'a> for RenderSystem<'a, R> { } // render player's world selection - if let Some((from, to)) = selected_block.bounds() { - self.renderer - .tile_selection(from, to, Color::rgb(230, 240, 230)); + if let Some((progress, (from, to))) = selected_block.bounds() { + let color = match progress { + SelectionProgress::InProgress => Color::rgb(140, 150, 140), + SelectionProgress::Complete => Color::rgb(230, 240, 230), + }; + self.renderer.tile_selection(from, to, color); } // render in-game ui elements above entities diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index d993fb3e..d77be8e1 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -3,7 +3,7 @@ use std::ops::{Add, Deref}; use common::*; use resources::Resources; use strum_macros::EnumDiscriminants; -use unit::world::{WorldPoint, WorldPosition, WorldPositionRange}; +use unit::world::{WorldPosition, WorldPositionRange}; use world::block::BlockType; use world::loader::{TerrainUpdatesRes, WorldTerrainUpdate}; use world::WorldChangeEvent; @@ -168,18 +168,19 @@ impl Simulation { self.apply_world_updates(world_viewer); // process backend input - if let Some((x, y)) = backend_data.mouse_position { + if let Some(point) = backend_data.mouse_position { let z = { let w = self.voxel_world.borrow(); let range = world_viewer.entity_range(); - let start_from = WorldPosition::new(*x as i32, *y as i32, range.top()); + let start_from = + WorldPosition::new(point.x() as i32, point.y() as i32, range.top()); match w.find_accessible_block_in_column_with_range(start_from, Some(range.bottom())) { Some(pos) => pos.2, None => range.bottom(), } }; - let pos = WorldPoint::new(*x, *y, z.slice() as f32).unwrap(); // not nan + let pos = point.into_world_point(NotNan::new(z.slice() as f32).unwrap()); // z is not nan self.ecs_world.insert(MouseLocation(pos)); } @@ -372,7 +373,7 @@ impl Simulation { UiRequest::FillSelectedTiles(placement, block_type) => { let selection = self.ecs_world.resource::(); - if let Some((mut from, mut to)) = selection.bounds() { + if let Some((mut from, mut to)) = selection.selected_bounds() { if let BlockPlacement::PlaceAbove = placement { // move the range up 1 block from = from.above(); diff --git a/renderer/engine/src/render/sdl/backend.rs b/renderer/engine/src/render/sdl/backend.rs index 0f56cf22..0db27b04 100644 --- a/renderer/engine/src/render/sdl/backend.rs +++ b/renderer/engine/src/render/sdl/backend.rs @@ -24,7 +24,7 @@ use resources::Resources; use sdl2::mouse::{MouseButton, MouseState, MouseWheelDirection}; use simulation::input::{InputEvent, SelectType, UiCommand, UiCommands, UiRequest, WorldColumn}; use std::hint::unreachable_unchecked; -use unit::world::{WorldPoint, WorldPosition}; +use unit::world::{WorldPoint, WorldPoint2d, WorldPosition}; pub struct SdlBackendPersistent { camera: Camera, @@ -236,7 +236,10 @@ impl InitializedSimulationBackend for SdlBackendInit { } => { if let Some(mouse_btn) = mousestate.pressed_mouse_buttons().next() { if let Some((select, col)) = self.parse_mouse_event(mouse_btn, x, y) { - self.selection.mouse_move(select, col); + let evt = self.selection.mouse_move(select, col); + if let Some(evt) = evt { + self.sim_input_events.push(evt); + } } } } @@ -269,7 +272,7 @@ impl InitializedSimulationBackend for SdlBackendInit { let x = NotNan::new(x).ok(); let y = NotNan::new(y).ok(); - x.zip(y) + x.zip(y).map(|(x, y)| WorldPoint2d::new(x, y)) }; BackendData { mouse_position } diff --git a/renderer/engine/src/render/sdl/selection.rs b/renderer/engine/src/render/sdl/selection.rs index aa137e59..d5d03ea5 100644 --- a/renderer/engine/src/render/sdl/selection.rs +++ b/renderer/engine/src/render/sdl/selection.rs @@ -1,56 +1,73 @@ use sdl2::mouse::MouseButton; -use simulation::input::{InputEvent, SelectType, WorldColumn}; +use simulation::input::{InputEvent, SelectType, SelectionProgress, WorldColumn}; #[derive(Copy, Clone)] enum MouseState { Unpressed, - Down(SelectType, WorldColumn), - Dragging(SelectType, WorldColumn), + Down(SelectType), + Dragging { + select: SelectType, + start: WorldColumn, + }, } -pub struct Selection { - state: MouseState, -} +pub struct Selection(MouseState); impl Selection { - pub fn mouse_down(&mut self, select: SelectType, pos: WorldColumn) { + pub fn mouse_down(&mut self, select: SelectType, _pos: WorldColumn) { // dont bother about multiple buttons being held down at once - if let MouseState::Unpressed = self.state { - self.state = MouseState::Down(select, pos); + if let MouseState::Unpressed = self.0 { + self.0 = MouseState::Down(select) } } pub fn mouse_up(&mut self, select: SelectType, pos: WorldColumn) -> Option { - let evt = match self.state { - MouseState::Down(prev_select, _) if select == prev_select => { + let evt = match self.0 { + MouseState::Down(prev_select) if select == prev_select => { // single selection at the mouse up location, ignoring the down location - let evt = InputEvent::Click(select, pos); - Some(evt) + Some(InputEvent::Click(select, pos)) } - MouseState::Dragging(prev_select, start) if select == prev_select => { + MouseState::Dragging { + select: prev_select, + start, + } if select == prev_select => { // region selection from original mouse down location - let evt = InputEvent::Select(select, start, pos); - Some(evt) + Some(InputEvent::Select { + select, + from: start, + to: pos, + progress: SelectionProgress::Complete, + }) } _ => None, }; if evt.is_some() { // consume mouse press - self.state = MouseState::Unpressed; + self.0 = MouseState::Unpressed; } evt } - pub fn mouse_move(&mut self, select: SelectType, pos: WorldColumn) { - match self.state { - MouseState::Down(prev_select, _) if prev_select == select => { + pub fn mouse_move(&mut self, select: SelectType, pos: WorldColumn) -> Option { + match self.0 { + MouseState::Down(prev_select) if prev_select == select => { // start dragging - self.state = MouseState::Dragging(select, pos); + self.0 = MouseState::Dragging { select, start: pos }; + None } - _ => {} + MouseState::Dragging { + select: prev_select, + start, + } if prev_select == select => Some(InputEvent::Select { + select, + from: start, + to: pos, + progress: SelectionProgress::InProgress, + }), + _ => None, } } @@ -65,8 +82,6 @@ impl Selection { impl Default for Selection { fn default() -> Self { - Self { - state: MouseState::Unpressed, - } + Selection(MouseState::Unpressed) } } diff --git a/renderer/engine/src/render/sdl/ui/windows/selection.rs b/renderer/engine/src/render/sdl/ui/windows/selection.rs index 5cd85a30..7bfe282d 100644 --- a/renderer/engine/src/render/sdl/ui/windows/selection.rs +++ b/renderer/engine/src/render/sdl/ui/windows/selection.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; use common::*; use simulation::input::{ - BlockPlacement, DivineInputCommand, SelectedEntity, SelectedTiles, UiRequest, + BlockPlacement, DivineInputCommand, SelectedEntity, SelectedTiles, SelectionProgress, UiRequest, }; use simulation::job::BuildThingJob; use simulation::{ @@ -594,11 +594,8 @@ impl SelectionWindow { } let selection = context.simulation().ecs.resource::(); - let bounds = match selection.bounds() { - None => { - context.text_disabled(im_str!("No tile selection")); - return; - } + let (progress, bounds) = match selection.bounds() { + None => return context.text_disabled(im_str!("No tile selection")), Some(bounds) => bounds, }; @@ -620,6 +617,18 @@ impl SelectionWindow { COLOR_BLUE, ); + context.key_value( + im_str!("Progress:"), + || { + ui_str!(in context, "{}", match progress { + SelectionProgress::Complete => "Selected", + SelectionProgress::InProgress => "Ongoing", + }) + }, + None, + COLOR_BLUE, + ); + context.key_value( im_str!("From:"), || ui_str!(in context, "{}", from), diff --git a/renderer/engine/src/render/sdl/ui/windows/society.rs b/renderer/engine/src/render/sdl/ui/windows/society.rs index 837d023a..cc2acaee 100644 --- a/renderer/engine/src/render/sdl/ui/windows/society.rs +++ b/renderer/engine/src/render/sdl/ui/windows/society.rs @@ -60,7 +60,7 @@ impl SocietyWindow { let block_selection = ecs.resource::(); // break selected blocks - if let Some(range) = block_selection.range() { + if let Some(range) = block_selection.selected_range() { any_buttons = true; if context.button(im_str!("Break blocks"), [0.0, 0.0]) { context.issue_request(UiRequest::IssueSocietyCommand( diff --git a/shared/unit/src/world/world_point.rs b/shared/unit/src/world/world_point.rs index 6f1b480d..d67fa51e 100644 --- a/shared/unit/src/world/world_point.rs +++ b/shared/unit/src/world/world_point.rs @@ -11,6 +11,9 @@ use std::fmt::Debug; #[derive(Copy, Clone, PartialEq, Default, PartialOrd, Hash, Ord)] pub struct WorldPoint(NotNan, NotNan, NotNan); +#[derive(Copy, Clone)] +pub struct WorldPoint2d(NotNan, NotNan); + #[inline] fn not_nan(x: f32) -> Option> { if x.is_finite() { @@ -130,6 +133,34 @@ impl WorldPoint { } } +impl WorldPoint2d { + /// None if any coord is not finite + pub fn new_checked(x: f32, y: f32) -> Option { + match (not_nan(x), not_nan(y)) { + (Some(x), Some(y)) => Some(Self(x, y)), + _ => None, + } + } + + pub fn new(x: NotNan, y: NotNan) -> Self { + Self(x, y) + } + + pub fn into_world_point(self, z: NotNan) -> WorldPoint { + WorldPoint(self.0, self.1, z) + } + + #[inline] + pub fn x(self) -> f32 { + self.0.into_inner() + } + + #[inline] + pub fn y(self) -> f32 { + self.1.into_inner() + } +} + impl From for Vector3 { fn from(p: WorldPoint) -> Self { Vector3 { From 512266f44ee6fadc5287315358e5c3bf3227e5c5 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Mon, 17 Jan 2022 11:08:03 +0200 Subject: [PATCH 05/46] Add block count to tile selection UI --- .planning/active.md | 2 +- game/simulation/src/input/system.rs | 245 +++++++++++++----- game/simulation/src/render/system.rs | 9 +- game/simulation/src/simulation.rs | 4 +- .../src/render/sdl/ui/windows/selection.rs | 35 ++- .../src/render/sdl/ui/windows/society.rs | 7 +- 6 files changed, 215 insertions(+), 87 deletions(-) diff --git a/.planning/active.md b/.planning/active.md index 9b1a80f2..9c552bac 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -5,7 +5,7 @@ * [X] show ui block selection as it is dragged * [ ] block selection can be added to with ctrl+drag * [ ] show selection dimensions in world - * [ ] use left click for all selections? + * [ ] use left click for all selections, to allow right click context menu * [ ] ui for creating jobs for building many blocks * [ ] ui for wall outline specifically? * hovering over button should show outline preview diff --git a/game/simulation/src/input/system.rs b/game/simulation/src/input/system.rs index 2eb47fe2..d30d03e4 100644 --- a/game/simulation/src/input/system.rs +++ b/game/simulation/src/input/system.rs @@ -2,6 +2,7 @@ use common::*; use unit::world::{WorldPosition, WorldPositionRange, WorldRange}; use crate::ecs::*; +pub use crate::input::system::selected_tiles::SelectedTiles; use crate::input::{InputEvent, SelectType, SelectionProgress, WorldColumn}; use crate::spatial::{Spatial, Transforms}; use crate::TransformComponent; @@ -22,13 +23,6 @@ pub struct SelectedComponent; /// `get()` will clear it if the entity is dead #[derive(Default)] pub struct SelectedEntity(Option); - -#[derive(Default, Clone)] -pub struct SelectedTiles { - current: Option<(WorldPositionRange, SelectionProgress)>, - last: Option<(WorldPosition, WorldPosition, SelectionProgress)>, -} - const TILE_SELECTION_LIMIT: f32 = 50.0; impl<'a> System<'a> for InputSystem<'a> { @@ -158,89 +152,204 @@ impl SelectedEntity { } } -impl SelectedTiles { - fn update( - &mut self, - range: (WorldColumn, WorldColumn), +mod selected_tiles { + use super::*; + use crate::InnerWorldRef; + use std::collections::BTreeMap; + use std::fmt::Write; + use world::block::BlockType; + + #[derive(Default, Clone)] + pub struct SelectedTiles { + current: Option, + last: Option<(WorldPosition, WorldPosition, SelectionProgress)>, + } + + #[derive(Clone)] + pub struct CurrentSelection { + range: WorldPositionRange, progress: SelectionProgress, - world: &WorldRef, - ) { - let (from, mut to) = range; + makeup: BTreeMap, + } + + pub struct BlockOccurrences<'a>(&'a BTreeMap); + + impl SelectedTiles { + pub fn update( + &mut self, + range: (WorldColumn, WorldColumn), + progress: SelectionProgress, + world: &WorldRef, + ) { + let (from, mut to) = range; + + // limit selection size by moving the second point placed. this isn't totally + // accurate and may allow selections of 1 block bigger, but meh who cares + let to = { + let dx = (from.x - to.x).abs(); + let dy = (from.y - to.y).abs(); + + let x_overlap = TILE_SELECTION_LIMIT - dx; + let y_overlap = TILE_SELECTION_LIMIT - dy; + + if x_overlap < 0.0 { + let mul = if from.x > to.x { 1.0 } else { -1.0 }; + to.x -= mul * x_overlap; + } + + if y_overlap < 0.0 { + let mul = if from.y > to.y { 1.0 } else { -1.0 }; + to.y -= mul * y_overlap; + } - // limit selection size by moving the second point placed. this isn't totally - // accurate and may allow selections of 1 block bigger, but meh who cares - let to = { - let dx = (from.x - to.x).abs(); - let dy = (from.y - to.y).abs(); + to + }; - let x_overlap = TILE_SELECTION_LIMIT - dx; - let y_overlap = TILE_SELECTION_LIMIT - dy; + let w = world.borrow(); + if let Some((min, max)) = from.find_min_max_walkable(&to, &w) { + let mut a = min.floor(); + let mut b = max.floor(); - if x_overlap < 0.0 { - let mul = if from.x > to.x { 1.0 } else { -1.0 }; - to.x -= mul * x_overlap; + // these blocks are walkable air blocks, move them down 1 to select the + // actual block beneath + a.2 -= 1; + b.2 -= 1; + + if Some((a, b, progress)) != self.last { + self.last = Some((a, b, progress)); + + self.current = Some(CurrentSelection::new( + WorldPositionRange::with_inclusive_range(a, b), + progress, + self.current.take(), + &w, + )); + + if let SelectionProgress::Complete = progress { + debug!("selecting tiles"; "min" => %a, "max" => %b); + } + } } + } - if y_overlap < 0.0 { - let mul = if from.y > to.y { 1.0 } else { -1.0 }; - to.y -= mul * y_overlap; + pub fn clear(&mut self) { + self.current = None; + } + + /// Includes in-progress selection + pub fn current(&self) -> Option<&CurrentSelection> { + self.current.as_ref() + } + + /// Only complete selection + pub fn current_selected(&self) -> Option<&CurrentSelection> { + match self.current.as_ref() { + Some( + sel @ CurrentSelection { + progress: SelectionProgress::Complete, + .. + }, + ) => Some(sel), + _ => None, } + } + } - to - }; + impl CurrentSelection { + fn new( + range: WorldPositionRange, + progress: SelectionProgress, + prev: Option, + world: &InnerWorldRef, + ) -> Self { + let mut makeup = prev + .map(|mut sel| { + sel.makeup.clear(); + sel.makeup + }) + .unwrap_or_default(); + + for (b, _) in world.iterate_blocks(&range) { + makeup + .entry(b.block_type()) + .and_modify(|count| *count += 1) + .or_insert(1); + } + + Self { + range, + progress, + makeup, + } + } - let w = world.borrow(); - if let Some((min, max)) = from.find_min_max_walkable(&to, &w) { - let mut a = min.floor(); - let mut b = max.floor(); + pub fn bounds(&self) -> (WorldPosition, WorldPosition) { + self.range.bounds() + } - // these blocks are walkable air blocks, move them down 1 to select the - // actual block beneath - a.2 -= 1; - b.2 -= 1; + pub fn progress(&self) -> SelectionProgress { + self.progress + } - if Some((a, b, progress)) != self.last { - self.last = Some((a, b, progress)); - self.current = Some((WorldPositionRange::with_inclusive_range(a, b), progress)); + pub fn range(&self) -> &WorldPositionRange { + &self.range + } - if let SelectionProgress::Complete = progress { - debug!("selecting tiles"; "min" => %a, "max" => %b); - } + pub fn single_tile(&self) -> Option { + match &self.range { + WorldRange::Single(pos) => Some(*pos), + WorldRange::Range(a, b) if a == b => Some(*a), + _ => None, } } - } - fn clear(&mut self) { - self.current = None; + pub fn block_occurrences(&self) -> impl Display + '_ { + BlockOccurrences(&self.makeup) + } } - /// Includes in-progress selection - pub fn bounds(&self) -> Option<(SelectionProgress, (WorldPosition, WorldPosition))> { - self.current - .as_ref() - .map(|(range, progress)| (*progress, range.bounds())) - } + impl Display for BlockOccurrences<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_char('[')?; + + let mut comma = false; + for (b, n) in self.0.iter() { + if comma { + f.write_str(", ")?; + } else { + comma = true; + } + write!(f, "{}x{}", n, b)?; + } - fn selected(&self) -> Option<&WorldPositionRange> { - match self.current.as_ref() { - Some((range, SelectionProgress::Complete)) => Some(range), - _ => None, + f.write_char(']') } } - pub fn selected_range(&self) -> Option { - self.selected().cloned() - } + #[cfg(test)] + mod tests { + use super::*; + use crate::input::system::selected_tiles::BlockOccurrences; + use std::collections::HashMap; + use world::block::BlockType; - pub fn selected_bounds(&self) -> Option<(WorldPosition, WorldPosition)> { - self.selected().map(|range| range.bounds()) - } + #[test] + fn comma_separated_blocks() { + let mut map = BTreeMap::new(); + + assert_eq!(format!("{}", BlockOccurrences(&map)), "[]"); + + map.insert(BlockType::Air, 5); + assert_eq!(format!("{}", BlockOccurrences(&map)), "[5xAir]"); - pub fn single_tile(&self) -> Option { - self.selected().and_then(|range| match *range { - WorldRange::Single(pos) => Some(pos), - WorldRange::Range(a, b) if a == b => Some(a), - _ => None, - }) + map.insert(BlockType::Dirt, 2); + assert_eq!(format!("{}", BlockOccurrences(&map)), "[5xAir, 2xDirt]"); + + map.insert(BlockType::Grass, 10); + assert_eq!( + format!("{}", BlockOccurrences(&map)), + "[5xAir, 2xDirt, 10xGrass]" + ); + } } } diff --git a/game/simulation/src/render/system.rs b/game/simulation/src/render/system.rs index fdfb3f00..cd831136 100644 --- a/game/simulation/src/render/system.rs +++ b/game/simulation/src/render/system.rs @@ -105,10 +105,11 @@ impl<'a, R: Renderer> System<'a> for RenderSystem<'a, R> { } // render player's world selection - if let Some((progress, (from, to))) = selected_block.bounds() { - let color = match progress { - SelectionProgress::InProgress => Color::rgb(140, 150, 140), - SelectionProgress::Complete => Color::rgb(230, 240, 230), + if let Some(sel) = selected_block.current() { + let (from, to) = sel.bounds(); + let color = match sel.progress() { + SelectionProgress::InProgress => Color::rgb(216, 221, 230), + SelectionProgress::Complete => Color::rgb(252, 253, 255), }; self.renderer.tile_selection(from, to, color); } diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index d77be8e1..5a6a001f 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -373,7 +373,9 @@ impl Simulation { UiRequest::FillSelectedTiles(placement, block_type) => { let selection = self.ecs_world.resource::(); - if let Some((mut from, mut to)) = selection.selected_bounds() { + if let Some((mut from, mut to)) = + selection.current_selected().map(|sel| sel.range().bounds()) + { if let BlockPlacement::PlaceAbove = placement { // move the range up 1 block from = from.above(); diff --git a/renderer/engine/src/render/sdl/ui/windows/selection.rs b/renderer/engine/src/render/sdl/ui/windows/selection.rs index 7bfe282d..016107f3 100644 --- a/renderer/engine/src/render/sdl/ui/windows/selection.rs +++ b/renderer/engine/src/render/sdl/ui/windows/selection.rs @@ -314,7 +314,10 @@ impl SelectionWindow { let tab = context.new_tab(im_str!("Control")); if tab.is_open() { let world_selection = ecs.resource::(); - if let Some(tile) = world_selection.single_tile() { + if let Some(tile) = world_selection + .current_selected() + .and_then(|sel| sel.single_tile()) + { if context.button(im_str!("Go to selected block"), [0.0, 0.0]) { context.issue_request(UiRequest::IssueDivineCommand( DivineInputCommand::Goto(tile.above()), @@ -593,13 +596,13 @@ impl SelectionWindow { return; } - let selection = context.simulation().ecs.resource::(); - let (progress, bounds) = match selection.bounds() { + let selection_res = context.simulation().ecs.resource::(); + let selection = match selection_res.current() { None => return context.text_disabled(im_str!("No tile selection")), - Some(bounds) => bounds, + Some(sel) => sel, }; - let (from, to) = bounds; + let (from, to) = selection.bounds(); let w = (to.0 - from.0).abs() + 1; let h = (to.1 - from.1).abs() + 1; let z = (to.2 - from.2).abs().slice() + 1; @@ -620,7 +623,7 @@ impl SelectionWindow { context.key_value( im_str!("Progress:"), || { - ui_str!(in context, "{}", match progress { + ui_str!(in context, "{}", match selection.progress() { SelectionProgress::Complete => "Selected", SelectionProgress::InProgress => "Ongoing", }) @@ -643,6 +646,13 @@ impl SelectionWindow { COLOR_ORANGE, ); + context.key_value( + im_str!("Blocks:"), + || ui_str!(in context, "{}", selection.block_occurrences()), + None, + COLOR_ORANGE, + ); + let tab_bar = context.new_tab_bar(im_str!("##worldselectiontabbar")); if !tab_bar.is_open() { return; @@ -652,7 +662,7 @@ impl SelectionWindow { { let tab = context.new_tab(im_str!("Block")); if tab.is_open() { - self.do_single_block(context, selection); + self.do_single_block(context, selection_res); } } @@ -661,7 +671,7 @@ impl SelectionWindow { { let tab = context.new_tab(im_str!("Generation")); if tab.is_open() { - self.do_generation(context, selection); + self.do_generation(context, selection_res); } } @@ -675,7 +685,10 @@ impl SelectionWindow { } fn do_single_block(&mut self, context: &UiContext, selection: &SelectedTiles) { - let pos = match selection.single_tile() { + let pos = match selection + .current_selected() + .and_then(|sel| sel.single_tile()) + { Some(pos) => pos, None => { context.text_disabled(im_str!("Single block selection required")); @@ -863,7 +876,9 @@ impl SelectionWindow { return; } - let single_tile = selection.single_tile(); + let single_tile = selection + .current_selected() + .and_then(|sel| sel.single_tile()); let block_query = single_tile.and_then(|pos| loader.query_block(pos)); let details = match (block_query, single_tile) { (None, Some(_)) => { diff --git a/renderer/engine/src/render/sdl/ui/windows/society.rs b/renderer/engine/src/render/sdl/ui/windows/society.rs index cc2acaee..6670d04d 100644 --- a/renderer/engine/src/render/sdl/ui/windows/society.rs +++ b/renderer/engine/src/render/sdl/ui/windows/society.rs @@ -58,14 +58,15 @@ impl SocietyWindow { let ecs = context.simulation().ecs; let block_selection = ecs.resource::(); + let block_selection = block_selection.current_selected(); // break selected blocks - if let Some(range) = block_selection.selected_range() { + if let Some(sel) = block_selection { any_buttons = true; if context.button(im_str!("Break blocks"), [0.0, 0.0]) { context.issue_request(UiRequest::IssueSocietyCommand( society_handle, - SocietyCommand::BreakBlocks(range), + SocietyCommand::BreakBlocks(sel.range().clone()), )); } } @@ -74,7 +75,7 @@ impl SocietyWindow { if let Some((entity, target)) = ecs .resource::() .get_unchecked() - .zip(block_selection.single_tile()) + .zip(block_selection.and_then(|sel| sel.single_tile())) { if ecs.is_entity_alive(entity) && ecs.has_component_by_name("haulable", entity) { let desc = context.description(entity); From 8d117ca6cb3133a9b7f8a5a260f8f1fd270fe7d9 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Mon, 17 Jan 2022 11:31:29 +0200 Subject: [PATCH 06/46] Update selection block counts on world change --- TODOS.md | 4 +- game/simulation/src/input/system.rs | 35 +++++++++---- game/simulation/src/simulation.rs | 79 ++++++++++++++++------------- shared/unit/src/world/range.rs | 8 +++ 4 files changed, 82 insertions(+), 44 deletions(-) diff --git a/TODOS.md b/TODOS.md index ec3069f6..5a792690 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,4 +1,4 @@ -# TODOs (400) +# TODOs (401) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) @@ -533,6 +533,8 @@ * `// TODO helper for this-1` * [shared/unit/src/lib.rs](shared/unit/src/lib.rs) (1) * `// TODO pub mod hunger;` + * [shared/unit/src/world/range.rs](shared/unit/src/world/range.rs) (1) + * `/// TODO cache this?` * [shared/unit/src/world/slab_position.rs](shared/unit/src/world/slab_position.rs) (1) * `// TODO consider using same generic pattern as SliceIndex for all points and positions` * [shared/unit/src/world/slice_index.rs](shared/unit/src/world/slice_index.rs) (1) diff --git a/game/simulation/src/input/system.rs b/game/simulation/src/input/system.rs index d30d03e4..2a5af5cd 100644 --- a/game/simulation/src/input/system.rs +++ b/game/simulation/src/input/system.rs @@ -253,6 +253,12 @@ mod selected_tiles { _ => None, } } + + pub fn on_world_change(&mut self, world: &WorldRef) { + self.current + .as_mut() + .map(|sel| sel.on_world_change(world.borrow())); + } } impl CurrentSelection { @@ -262,24 +268,35 @@ mod selected_tiles { prev: Option, world: &InnerWorldRef, ) -> Self { - let mut makeup = prev + let makeup = prev .map(|mut sel| { sel.makeup.clear(); sel.makeup }) .unwrap_or_default(); - for (b, _) in world.iterate_blocks(&range) { - makeup - .entry(b.block_type()) - .and_modify(|count| *count += 1) - .or_insert(1); - } - - Self { + let mut sel = Self { range, progress, makeup, + }; + + sel.update_makeup(world); + + sel + } + + fn on_world_change(&mut self, world: InnerWorldRef) { + self.update_makeup(&world); + } + + fn update_makeup(&mut self, world: &InnerWorldRef) { + self.makeup.clear(); + for (b, _) in world.iterate_blocks(&self.range) { + self.makeup + .entry(b.block_type()) + .and_modify(|count| *count += 1) + .or_insert(1); } } diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index 5a6a001f..5976ca3f 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -342,15 +342,11 @@ impl Simulation { // consume change events let mut events = std::mem::take(&mut self.change_events); - events - .drain(..) - .filter(|e| e.new != e.prev) - .for_each(|e| self.on_world_change(e)); + self.on_world_changes(&events); + events.clear(); // swap storage back and forget empty vec - let empty = std::mem::replace(&mut self.change_events, events); - debug_assert!(empty.is_empty()); - std::mem::forget(empty); + std::mem::forget(std::mem::replace(&mut self.change_events, events)); } fn process_ui_commands(&mut self, commands: impl Iterator) -> Option { @@ -551,39 +547,54 @@ impl Simulation { renderer.deinit() } - fn on_world_change(&mut self, WorldChangeEvent { pos, prev, new }: WorldChangeEvent) { - debug_assert_ne!(prev, new); + fn on_world_changes(&mut self, events: &[WorldChangeEvent]) { + let selection = self.ecs_world.resource_mut::(); + let mut selection_modified = false; - match (prev, new) { - (_, BlockType::Chest) => { - // new chest placed - if let Err(err) = self - .ecs_world - .helpers_containers() - .create_container_voxel(pos, "core_storage_chest") - { - error!("failed to create container entity"; "error" => %err); + for &WorldChangeEvent { pos, prev, new } in events { + match (prev, new) { + (a, b) if a == b => continue, + (_, BlockType::Chest) => { + // new chest placed + if let Err(err) = self + .ecs_world + .helpers_containers() + .create_container_voxel(pos, "core_storage_chest") + { + error!("failed to create container entity"; "error" => %err); + } + } + + (BlockType::Chest, _) => { + // chest destroyed + self.ecs_world.resource::().queue( + "destroy container", + move |world| match world + .helpers_containers() + .destroy_container(pos, DeathReason::BlockDestroyed) + { + Err(err) => { + error!("failed to destroy container"; "error" => %err); + Err(err.into()) + } + Ok(()) => Ok(()), + }, + ) } + _ => {} } - (BlockType::Chest, _) => { - // chest destroyed - self.ecs_world.resource::().queue( - "destroy container", - move |world| match world - .helpers_containers() - .destroy_container(pos, DeathReason::BlockDestroyed) - { - Err(err) => { - error!("failed to destroy container"; "error" => %err); - Err(err.into()) - } - Ok(()) => Ok(()), - }, - ) + if !selection_modified { + if let Some(sel) = selection.current_selected() { + if sel.range().contains(&pos) { + selection_modified = true; + } + } } + } - _ => {} + if selection_modified { + selection.on_world_change(&self.voxel_world); } } diff --git a/shared/unit/src/world/range.rs b/shared/unit/src/world/range.rs index beded34e..ba51ea93 100644 --- a/shared/unit/src/world/range.rs +++ b/shared/unit/src/world/range.rs @@ -86,6 +86,7 @@ impl WorldRange

{ } /// (min x, max x), (min y, max y), (min z, max z) inclusive + /// TODO cache this? #[allow(clippy::type_complexity)] pub fn ranges(&self) -> ((P::XY, P::XY), (P::XY, P::XY), (P::Z, P::Z)) { let (from, to) = match self { @@ -132,6 +133,13 @@ impl WorldRange

{ .map(|(from, to)| WorldRange::Range(from, to)), } } + + pub fn contains(&self, pos: &P) -> bool { + let ((ax, bx), (ay, by), (az, bz)) = self.ranges(); + let (x, y, z) = pos.xyz(); + + ax <= x && x <= bx && ay <= y && y <= by && az <= z && z <= bz + } } impl> Add<(XY, XY, Z)> From a9dbeb468b946b97c587b3dad09390fd18be07bf Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Mon, 17 Jan 2022 16:08:38 +0200 Subject: [PATCH 07/46] Wrap block makeup UI field --- renderer/engine/src/render/sdl/ui/windows/selection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/engine/src/render/sdl/ui/windows/selection.rs b/renderer/engine/src/render/sdl/ui/windows/selection.rs index 016107f3..e3ba19d1 100644 --- a/renderer/engine/src/render/sdl/ui/windows/selection.rs +++ b/renderer/engine/src/render/sdl/ui/windows/selection.rs @@ -648,7 +648,7 @@ impl SelectionWindow { context.key_value( im_str!("Blocks:"), - || ui_str!(in context, "{}", selection.block_occurrences()), + || Value::Wrapped(ui_str!(in context, "{}", selection.block_occurrences())), None, COLOR_ORANGE, ); From 1d4e737d28a29a115c6933297871b63317fac8e6 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Mon, 17 Jan 2022 16:32:59 +0200 Subject: [PATCH 08/46] Add buttons to move selection up and down --- TODOS.md | 4 ++- game/simulation/src/input/command.rs | 8 +++++ game/simulation/src/input/system.rs | 33 +++++++++++++++++++ game/simulation/src/simulation.rs | 5 +++ .../src/render/sdl/ui/windows/selection.rs | 21 +++++++++++- shared/unit/src/world/range.rs | 17 +++++++++- 6 files changed, 85 insertions(+), 3 deletions(-) diff --git a/TODOS.md b/TODOS.md index 5a792690..8b049236 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,4 +1,4 @@ -# TODOs (401) +# TODOs (402) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) @@ -228,6 +228,8 @@ * `// TODO move this into Tick` * `// TODO sort by elapsed() bool instead` * `// TODO might be better to just insert sorted` + * [game/simulation/src/input/command.rs](game/simulation/src/input/command.rs) (1) + * `// TODO expand/contract in a direction` * [game/simulation/src/input/system.rs](game/simulation/src/input/system.rs) (3) * `// TODO multiple clicks in the same place should iterate through all entities in selection range` * `// TODO spatial lookup for ui elements too` diff --git a/game/simulation/src/input/command.rs b/game/simulation/src/input/command.rs index 4301c2f3..bfa35b0e 100644 --- a/game/simulation/src/input/command.rs +++ b/game/simulation/src/input/command.rs @@ -41,6 +41,14 @@ pub enum UiRequest { entity: Entity, enabled: bool, }, + + ModifySelection(SelectionModification), +} + +pub enum SelectionModification { + Up, + Down, + // TODO expand/contract in a direction } pub enum UiResponsePayload { diff --git a/game/simulation/src/input/system.rs b/game/simulation/src/input/system.rs index 2a5af5cd..d96966f8 100644 --- a/game/simulation/src/input/system.rs +++ b/game/simulation/src/input/system.rs @@ -154,9 +154,11 @@ impl SelectedEntity { mod selected_tiles { use super::*; + use crate::input::SelectionModification; use crate::InnerWorldRef; use std::collections::BTreeMap; use std::fmt::Write; + use unit::world::{all_slabs_in_range, SlabLocation}; use world::block::BlockType; #[derive(Default, Clone)] @@ -254,6 +256,18 @@ mod selected_tiles { } } + pub fn modify(&mut self, modification: SelectionModification, world: &WorldRef) { + if let Some( + sel @ CurrentSelection { + progress: SelectionProgress::Complete, + .. + }, + ) = self.current.as_mut() + { + sel.modify(modification, world); + } + } + pub fn on_world_change(&mut self, world: &WorldRef) { self.current .as_mut() @@ -323,6 +337,25 @@ mod selected_tiles { pub fn block_occurrences(&self) -> impl Display + '_ { BlockOccurrences(&self.makeup) } + + pub fn modify(&mut self, modification: SelectionModification, world: &WorldRef) { + let new = match modification { + SelectionModification::Up => self.range.above(), + SelectionModification::Down => self.range.below(), + }; + + if let Some(new) = new { + let w = world.borrow(); + let (from, to) = new.bounds(); + + if w.has_slab(SlabLocation::new(from.slice().slab_index(), from)) + && w.has_slab(SlabLocation::new(to.slice().slab_index(), to)) + { + self.range = new; + self.update_makeup(&world.borrow()) + } + } + } } impl Display for BlockOccurrences<'_> { diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index 5976ca3f..28f3725d 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -483,6 +483,11 @@ impl Simulation { let _ = self.ecs_world.remove_now::(entity); } } + + UiRequest::ModifySelection(modification) => { + let sel = self.ecs_world.resource_mut::(); + sel.modify(modification, &self.voxel_world); + } } } diff --git a/renderer/engine/src/render/sdl/ui/windows/selection.rs b/renderer/engine/src/render/sdl/ui/windows/selection.rs index e3ba19d1..9a7d883e 100644 --- a/renderer/engine/src/render/sdl/ui/windows/selection.rs +++ b/renderer/engine/src/render/sdl/ui/windows/selection.rs @@ -4,7 +4,8 @@ use std::cell::RefCell; use common::*; use simulation::input::{ - BlockPlacement, DivineInputCommand, SelectedEntity, SelectedTiles, SelectionProgress, UiRequest, + BlockPlacement, DivineInputCommand, SelectedEntity, SelectedTiles, SelectionModification, + SelectionProgress, UiRequest, }; use simulation::job::BuildThingJob; use simulation::{ @@ -646,6 +647,24 @@ impl SelectionWindow { COLOR_ORANGE, ); + // selection modification buttons + { + let mut modification = None; + if context.button(im_str!("Move up"), [0.0, 0.0]) { + modification = Some(SelectionModification::Up); + } + + context.ui().same_line(0.0); + + if context.button(im_str!("Move down"), [0.0, 0.0]) { + modification = Some(SelectionModification::Down); + } + + if let Some(modification) = modification { + context.issue_request(UiRequest::ModifySelection(modification)); + } + } + context.key_value( im_str!("Blocks:"), || Value::Wrapped(ui_str!(in context, "{}", selection.block_occurrences())), diff --git a/shared/unit/src/world/range.rs b/shared/unit/src/world/range.rs index ba51ea93..eae6ed3b 100644 --- a/shared/unit/src/world/range.rs +++ b/shared/unit/src/world/range.rs @@ -36,6 +36,11 @@ pub trait RangePosition: Copy + Sized { let (x, y, z) = self.xyz(); Self::new((x, y, z - Self::Z::one())) } + + fn above(self) -> Option { + let (x, y, z) = self.xyz(); + Self::new((x, y, z + Self::Z::one())) + } } impl WorldRange

{ @@ -124,7 +129,7 @@ impl WorldRange

{ xy.as_() * z.as_() } - pub fn below(self) -> Option { + pub fn below(&self) -> Option { match self { WorldRange::Single(pos) => pos.below().map(WorldRange::Single), WorldRange::Range(from, to) => from @@ -134,6 +139,16 @@ impl WorldRange

{ } } + pub fn above(&self) -> Option { + match self { + WorldRange::Single(pos) => pos.above().map(WorldRange::Single), + WorldRange::Range(from, to) => from + .above() + .zip(to.above()) + .map(|(from, to)| WorldRange::Range(from, to)), + } + } + pub fn contains(&self, pos: &P) -> bool { let ((ax, bx), (ay, by), (az, bz)) = self.ranges(); let (x, y, z) = pos.xyz(); From 2de903293aa1c5e3cb86cb39da27f4499eeda864 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Mon, 17 Jan 2022 16:51:39 +0200 Subject: [PATCH 09/46] Replace bincode with ron for UI state --- Cargo.lock | 2 +- TODOS.md | 5 +++-- renderer/engine/Cargo.toml | 2 +- renderer/engine/src/render/sdl/ui/render.rs | 13 ++++++------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e175defd..d4263610 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -792,7 +792,6 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" name = "engine" version = "0.1.0" dependencies = [ - "bincode", "color", "common", "config", @@ -803,6 +802,7 @@ dependencies = [ "imgui-sdl2", "panik", "resources", + "ron", "rusttype", "sdl2", "sdl2-sys", diff --git a/TODOS.md b/TODOS.md index 8b049236..ace3e826 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,4 +1,4 @@ -# TODOs (402) +# TODOs (403) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) @@ -503,7 +503,8 @@ * `// TODO use the arena for this` * `// TODO other job tabs` * `// TODO proper way of checking if an entity is living` - * [renderer/engine/src/render/sdl/ui/windows/society.rs](renderer/engine/src/render/sdl/ui/windows/society.rs) (2) + * [renderer/engine/src/render/sdl/ui/windows/society.rs](renderer/engine/src/render/sdl/ui/windows/society.rs) (3) + * `// TODO temporary hardcoded list of builds` * `// TODO preserve finished jobs and tasks for a bit and display them in the ui too` * `// TODO use table API when available` * [renderer/main/src/main.rs](renderer/main/src/main.rs) (5) diff --git a/renderer/engine/Cargo.toml b/renderer/engine/Cargo.toml index 62309abb..edac65e1 100644 --- a/renderer/engine/Cargo.toml +++ b/renderer/engine/Cargo.toml @@ -30,7 +30,7 @@ imgui = { version = "0.7", optional = true } rusttype = { version = "0.9", optional = true, features = ["gpu_cache"] } serde = { version = "1.0", features = ["derive"] } -bincode = "1.3" +ron = "0.6" [features] default = ["common/log-to-file", "panik/use-slog", "common/binary", "scripting"] diff --git a/renderer/engine/src/render/sdl/ui/render.rs b/renderer/engine/src/render/sdl/ui/render.rs index d68b1e6a..b77a0c5f 100644 --- a/renderer/engine/src/render/sdl/ui/render.rs +++ b/renderer/engine/src/render/sdl/ui/render.rs @@ -166,8 +166,9 @@ impl Ui { Ok(Some(state)) } - fn serialize_to(&mut self, writer: impl Write) -> Result<(), bincode::Error> { + fn serialize_to(&mut self, writer: impl Write) -> Result<(), ron::Error> { #[derive(Serialize)] + #[repr(C)] struct SerializedState<'a> { state: &'a State, imgui: &'a str, @@ -181,20 +182,18 @@ impl Ui { imgui: &imgui, }; - bincode::serialize_into(writer, &serialized) + ron::ser::to_writer(writer, &serialized) } - fn deserialize_from( - imgui_ctx: &mut Context, - reader: impl Read, - ) -> Result { + fn deserialize_from(imgui_ctx: &mut Context, reader: impl Read) -> Result { #[derive(Deserialize)] + #[repr(C)] struct SerializedState { state: State, imgui: String, } - let SerializedState { state, imgui } = bincode::deserialize_from(reader)?; + let SerializedState { state, imgui } = ron::de::from_reader(reader)?; imgui_ctx.load_ini_settings(&imgui); Ok(state) From f6797c816dde691909066192905e340effccbbcc Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Tue, 18 Jan 2022 11:35:11 +0200 Subject: [PATCH 10/46] Add UI button to create multiple build jobs --- .planning/active.md | 5 +- TODOS.md | 11 +++-- game/simulation/src/build/builds.rs | 8 ++-- game/simulation/src/input/system.rs | 2 +- game/simulation/src/physics/bounds.rs | 19 ++++---- game/simulation/src/society/job/job.rs | 12 ++++- game/world/src/world.rs | 18 ++++--- .../src/render/sdl/ui/windows/society.rs | 47 +++++++++++++++++-- shared/unit/src/world/range.rs | 12 ++++- 9 files changed, 98 insertions(+), 36 deletions(-) diff --git a/.planning/active.md b/.planning/active.md index 9c552bac..4571cd35 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -6,10 +6,13 @@ * [ ] block selection can be added to with ctrl+drag * [ ] show selection dimensions in world * [ ] use left click for all selections, to allow right click context menu -* [ ] ui for creating jobs for building many blocks +* [X] ui for creating jobs for building many blocks * [ ] ui for wall outline specifically? * hovering over button should show outline preview * [ ] can specify thickness of wall * [ ] can shrink/expand selection by 1 block * [ ] extend material reservation to include the specific materials in transit for a build, to avoid others considering hauling more when it's already on the way +* [ ] register builds in data +* [ ] central registry of all builds including a unique identifier to refer to it in ui and its + requests diff --git a/TODOS.md b/TODOS.md index ace3e826..00d390df 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,4 +1,4 @@ -# TODOs (403) +# TODOs (405) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) @@ -330,8 +330,10 @@ * `// TODO sort out systems so they all have an ecs_world reference and can keep state` * `// TODO limit time/count` * `let discovered = empty(); // TODO include slabs discovered by members of player's society` - * [game/simulation/src/society/job/job.rs](game/simulation/src/society/job/job.rs) (1) + * [game/simulation/src/society/job/job.rs](game/simulation/src/society/job/job.rs) (3) * `/// TODO provide size hint that could be used as an optimisation for a small number of tasks (e.g. smallvec)` + * `// TODO specify which build` + * `// TODO use actual job provided` * [game/simulation/src/society/job/jobs/break_blocks.rs](game/simulation/src/society/job/jobs/break_blocks.rs) (1) * `// TODO add display impl for WorldPositionRange` * [game/simulation/src/society/job/jobs/build.rs](game/simulation/src/society/job/jobs/build.rs) (10) @@ -503,7 +505,8 @@ * `// TODO use the arena for this` * `// TODO other job tabs` * `// TODO proper way of checking if an entity is living` - * [renderer/engine/src/render/sdl/ui/windows/society.rs](renderer/engine/src/render/sdl/ui/windows/society.rs) (3) + * [renderer/engine/src/render/sdl/ui/windows/society.rs](renderer/engine/src/render/sdl/ui/windows/society.rs) (4) + * `// TODO handle failure better?` * `// TODO temporary hardcoded list of builds` * `// TODO preserve finished jobs and tasks for a bit and display them in the ui too` * `// TODO use table API when available` @@ -536,8 +539,6 @@ * `// TODO helper for this-1` * [shared/unit/src/lib.rs](shared/unit/src/lib.rs) (1) * `// TODO pub mod hunger;` - * [shared/unit/src/world/range.rs](shared/unit/src/world/range.rs) (1) - * `/// TODO cache this?` * [shared/unit/src/world/slab_position.rs](shared/unit/src/world/slab_position.rs) (1) * `// TODO consider using same generic pattern as SliceIndex for all points and positions` * [shared/unit/src/world/slice_index.rs](shared/unit/src/world/slice_index.rs) (1) diff --git a/game/simulation/src/build/builds.rs b/game/simulation/src/build/builds.rs index dfe21ff4..23798dc9 100644 --- a/game/simulation/src/build/builds.rs +++ b/game/simulation/src/build/builds.rs @@ -1,11 +1,12 @@ // these will be defined in data at some point use crate::build::material::BuildMaterial; -use std::fmt::Debug; +use common::Display; +use std::fmt; use std::num::NonZeroU16; use world::block::BlockType; -pub trait Build: Debug { +pub trait Build: fmt::Debug + fmt::Display { /// Target block fn output(&self) -> BlockType; @@ -18,7 +19,8 @@ pub trait Build: Debug { // ------- -#[derive(Debug)] +/// Stone brick wall +#[derive(Debug, Display)] pub struct StoneBrickWall; impl Build for StoneBrickWall { diff --git a/game/simulation/src/input/system.rs b/game/simulation/src/input/system.rs index d96966f8..9dc606f3 100644 --- a/game/simulation/src/input/system.rs +++ b/game/simulation/src/input/system.rs @@ -306,7 +306,7 @@ mod selected_tiles { fn update_makeup(&mut self, world: &InnerWorldRef) { self.makeup.clear(); - for (b, _) in world.iterate_blocks(&self.range) { + for (b, _) in world.iterate_blocks(self.range.clone()) { self.makeup .entry(b.block_type()) .and_modify(|count| *count += 1) diff --git a/game/simulation/src/physics/bounds.rs b/game/simulation/src/physics/bounds.rs index dc6e7775..8ae63856 100644 --- a/game/simulation/src/physics/bounds.rs +++ b/game/simulation/src/physics/bounds.rs @@ -115,7 +115,7 @@ impl BoundsOverlap { impl BoundsCheck for World { fn all(&self, range: &WorldPositionRange) -> Option { let mut opacities = self - .iterate_blocks(range) + .iterate_blocks(range.clone()) .map(|(block, _)| block.block_type().opacity()); opacities.next().and_then(|opacity| { @@ -130,13 +130,16 @@ impl BoundsCheck for World { } fn find_solids(&self, range: &WorldPositionRange, out: &mut Vec) { - out.extend(self.iterate_blocks(range).filter_map(|(block, pos)| { - if block.opacity().solid() { - Some(pos) - } else { - None - } - })); + out.extend( + self.iterate_blocks(range.clone()) + .filter_map(|(block, pos)| { + if block.opacity().solid() { + Some(pos) + } else { + None + } + }), + ); } } diff --git a/game/simulation/src/society/job/job.rs b/game/simulation/src/society/job/job.rs index 99ca1835..15c9be70 100644 --- a/game/simulation/src/society/job/job.rs +++ b/game/simulation/src/society/job/job.rs @@ -1,5 +1,5 @@ use crate::job::SocietyTask; -use crate::{EcsWorld, Entity, WorldPositionRange}; +use crate::{Build, EcsWorld, Entity, StoneBrickWall, WorldPositionRange}; use std::any::Any; use std::borrow::BorrowMut; use std::cell::RefCell; @@ -10,7 +10,7 @@ use crate::job::list::SocietyJobHandle; use crate::society::Society; use std::ops::Deref; use std::rc::Rc; -use unit::world::WorldPoint; +use unit::world::{WorldPoint, WorldPosition}; /// A high-level society job that produces a number of [SocietyTask]s. Unsized but it lives in an /// Rc anyway @@ -65,6 +65,8 @@ pub trait SocietyJobImpl: Display + Debug { #[derive(Debug)] pub enum SocietyCommand { BreakBlocks(WorldPositionRange), + // TODO specify which build + Build(WorldPositionRange), HaulToPosition(Entity, WorldPoint), /// (thing, container) @@ -87,6 +89,12 @@ impl SocietyCommand { match self { BreakBlocks(range) => job!(BreakBlocksJob::new(range)), + Build(pos) => { + for block in pos.iter_blocks() { + // TODO use actual job provided + jobs.submit(world, BuildThingJob::new(block, StoneBrickWall)) + } + } HaulToPosition(e, pos) => { job!(HaulJob::with_target_position(e, pos, world).ok_or(self)?) } diff --git a/game/world/src/world.rs b/game/world/src/world.rs index 3b471172..5dd75a6b 100644 --- a/game/world/src/world.rs +++ b/game/world/src/world.rs @@ -607,15 +607,13 @@ impl World { } } - pub fn iterate_blocks<'a>( - &'a self, - range: &WorldPositionRange, - ) -> impl Iterator + 'a { - let ((ax, bx), (ay, by), (az, bz)) = range.ranges(); - (az..=bz) - .cartesian_product(ay..=by) - .cartesian_product(ax..=bx) - .map(move |((z, y), x)| (self.block((x, y, z)), (x, y, z).into())) + pub fn iterate_blocks( + &self, + range: WorldPositionRange, + ) -> impl Iterator + '_ { + range + .iter_blocks() + .map(move |pos| (self.block(pos), pos)) .filter_map(move |(block, pos)| block.map(|b| (b, pos))) } @@ -626,7 +624,7 @@ impl World { ) -> impl Iterator + 'a { // TODO benchmark filter_blocks_in_range, then optimize slab and slice lookups - self.iterate_blocks(range) + self.iterate_blocks(range.clone()) .filter(move |(block, pos)| f(*block, pos)) } diff --git a/renderer/engine/src/render/sdl/ui/windows/society.rs b/renderer/engine/src/render/sdl/ui/windows/society.rs index 6670d04d..d5fb4968 100644 --- a/renderer/engine/src/render/sdl/ui/windows/society.rs +++ b/renderer/engine/src/render/sdl/ui/windows/society.rs @@ -1,7 +1,10 @@ -use imgui::{im_str, StyleColor}; +use imgui::{im_str, ChildWindow, Selectable, StyleColor}; use simulation::input::{SelectedEntity, SelectedTiles, UiRequest}; -use simulation::{AssociatedBlockData, ComponentWorld, PlayerSociety, Societies, SocietyHandle}; +use simulation::{ + AssociatedBlockData, Build, ComponentWorld, PlayerSociety, Societies, SocietyHandle, + StoneBrickWall, +}; use crate::render::sdl::ui::context::{DefaultOpen, UiContext}; use crate::render::sdl::ui::windows::{UiExt, COLOR_BLUE}; @@ -10,7 +13,9 @@ use serde::{Deserialize, Serialize}; use simulation::job::SocietyCommand; #[derive(Default, Serialize, Deserialize)] -pub struct SocietyWindow; +pub struct SocietyWindow { + build_selection: usize, +} impl SocietyWindow { pub fn render(&mut self, context: &UiContext) { @@ -51,7 +56,7 @@ impl SocietyWindow { self.do_jobs(context, society_handle); } - fn do_control(&self, context: &UiContext, society_handle: SocietyHandle) { + fn do_control(&mut self, context: &UiContext, society_handle: SocietyHandle) { let tab = context.new_tab(im_str!("Control")); if tab.is_open() { let mut any_buttons = false; @@ -63,12 +68,44 @@ impl SocietyWindow { // break selected blocks if let Some(sel) = block_selection { any_buttons = true; - if context.button(im_str!("Break blocks"), [0.0, 0.0]) { + if context.button( + ui_str!(in context, "Break {} blocks", sel.range().count()), + [0.0, 0.0], + ) { context.issue_request(UiRequest::IssueSocietyCommand( society_handle, SocietyCommand::BreakBlocks(sel.range().clone()), )); } + + if context.button(im_str!("Build"), [0.0, 0.0]) { + // TODO handle failure better? + if let Some(above) = sel.range().above() { + context.issue_request(UiRequest::IssueSocietyCommand( + society_handle, + SocietyCommand::Build(above), + )); + } + } + + context.same_line_with_spacing(0.0, 40.0); + + ChildWindow::new("##buildsocietyblocks") + .size([0.0, 50.0]) + .horizontal_scrollbar(true) + .movable(false) + .build(context.ui(), || { + // TODO temporary hardcoded list of builds + let builds = [&StoneBrickWall as &dyn Build]; + for (i, build) in builds.iter().enumerate() { + if Selectable::new(ui_str!(in context, "{}", build)) + .selected(self.build_selection == i) + .build(context) + { + self.build_selection = i; + } + } + }); } // entity selection and block selection diff --git a/shared/unit/src/world/range.rs b/shared/unit/src/world/range.rs index eae6ed3b..fb7e444e 100644 --- a/shared/unit/src/world/range.rs +++ b/shared/unit/src/world/range.rs @@ -4,6 +4,7 @@ use crate::world::{ BlockCoord, BlockPosition, GlobalSliceIndex, LocalSliceIndex, SlabPosition, WorldPoint, WorldPosition, }; +use common::Itertools; use std::hash::{Hash, Hasher}; use std::ops::{Add, Mul, SubAssign}; @@ -91,7 +92,6 @@ impl WorldRange

{ } /// (min x, max x), (min y, max y), (min z, max z) inclusive - /// TODO cache this? #[allow(clippy::type_complexity)] pub fn ranges(&self) -> ((P::XY, P::XY), (P::XY, P::XY), (P::Z, P::Z)) { let (from, to) = match self { @@ -288,6 +288,16 @@ impl PartialEq for WorldRange

{ } } +impl WorldRange { + pub fn iter_blocks(self) -> impl Iterator { + let ((ax, bx), (ay, by), (az, bz)) = self.ranges(); + (az..=bz) + .cartesian_product(ay..=by) + .cartesian_product(ax..=bx) + .map(move |((z, y), x)| (x, y, z).into()) + } +} + #[cfg(test)] mod tests { use crate::world::{WorldPoint, WorldPointRange, WorldPositionRange}; From 730fc215db8b2c015cc9ebec7b94aab82905119d Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Tue, 18 Jan 2022 11:40:04 +0200 Subject: [PATCH 11/46] Rename definition registry to be more explicit --- .../simulation/src/definitions/loader/load.rs | 12 +++---- game/simulation/src/definitions/mod.rs | 2 +- game/simulation/src/definitions/registry.rs | 32 ++++++++----------- game/simulation/src/ecs/world.rs | 6 ++-- game/simulation/src/society/job/jobs/build.rs | 4 +-- 5 files changed, 25 insertions(+), 31 deletions(-) diff --git a/game/simulation/src/definitions/loader/load.rs b/game/simulation/src/definitions/loader/load.rs index 22ed4582..1813f5da 100644 --- a/game/simulation/src/definitions/loader/load.rs +++ b/game/simulation/src/definitions/loader/load.rs @@ -4,17 +4,17 @@ use crate::definitions::loader::step1_deserialization::{ use crate::definitions::loader::step2_preprocessing::preprocess; use crate::definitions::loader::step3_construction::{instantiate, Definition}; use crate::definitions::loader::template_lookup::TemplateLookup; -use crate::definitions::registry::RegistryBuilder; -use crate::definitions::{DefinitionError, DefinitionErrors, Registry}; +use crate::definitions::registry::DefinitionRegistryBuilder; +use crate::definitions::{DefinitionError, DefinitionErrors, DefinitionRegistry}; -pub fn load(resources: resources::Definitions) -> Result { +pub fn load(resources: resources::Definitions) -> Result { let defs = load_and_preprocess_with(|| collect_raw_definitions(resources))?; let instantiated = instantiate(defs, &TemplateLookup::init())?; build_registry(instantiated) } #[cfg(test)] -pub fn load_from_str(definitions: &str) -> Result { +pub fn load_from_str(definitions: &str) -> Result { let defs = preprocess_from_str(definitions)?; let instantiated = instantiate(defs, &TemplateLookup::init())?; build_registry(instantiated) @@ -58,10 +58,10 @@ pub fn load_and_preprocess_with< pub fn build_registry( defs: Vec<(DefinitionUid, Definition)>, -) -> Result { +) -> Result { let mut errors = Vec::new(); - let mut registry = RegistryBuilder::new(); + let mut registry = DefinitionRegistryBuilder::new(); for (uid, definition) in defs { if let Err((def, err)) = registry.register(uid, definition) { errors.push(def.make_error(err)); diff --git a/game/simulation/src/definitions/mod.rs b/game/simulation/src/definitions/mod.rs index 8f662076..e2a4349f 100644 --- a/game/simulation/src/definitions/mod.rs +++ b/game/simulation/src/definitions/mod.rs @@ -6,7 +6,7 @@ mod registry; pub use builder::{BuilderError, DefinitionBuilder, EntityPosition}; pub use component::DefinitionNameComponent; pub use loader::{load, Definition, ValueImpl}; -pub use registry::Registry; +pub use registry::DefinitionRegistry; #[cfg(test)] pub use loader::load_from_str; diff --git a/game/simulation/src/definitions/registry.rs b/game/simulation/src/definitions/registry.rs index 03484b32..93c7a7cb 100644 --- a/game/simulation/src/definitions/registry.rs +++ b/game/simulation/src/definitions/registry.rs @@ -7,19 +7,13 @@ use crate::ComponentWorld; use common::*; use std::collections::HashMap; -pub struct Registry { - map: HashMap, -} +pub struct DefinitionRegistry(HashMap); -pub struct RegistryBuilder { - map: HashMap, -} +pub struct DefinitionRegistryBuilder(HashMap); -impl RegistryBuilder { +impl DefinitionRegistryBuilder { pub fn new() -> Self { - Self { - map: HashMap::with_capacity(512), - } + Self(HashMap::with_capacity(512)) } pub fn register( @@ -28,37 +22,37 @@ impl RegistryBuilder { definition: Definition, ) -> Result<(), (Definition, DefinitionErrorKind)> { #[allow(clippy::map_entry)] - if self.map.contains_key(&uid) { + if self.0.contains_key(&uid) { Err((definition, DefinitionErrorKind::AlreadyRegistered(uid))) } else { - self.map.insert(uid, definition); + self.0.insert(uid, definition); Ok(()) } } - pub fn build(self) -> Registry { + pub fn build(self) -> DefinitionRegistry { info!( "creating definition registry with {count} entries", - count = self.map.len() + count = self.0.len() ); - Registry { map: self.map } + DefinitionRegistry(self.0) } } -impl Registry { +impl DefinitionRegistry { pub fn instantiate<'s, 'w: 's, W: ComponentWorld>( &'s self, uid: &str, world: &'w W, ) -> Result, DefinitionErrorKind> { - match self.map.get(uid) { + match self.0.get(uid) { Some(def) => Ok(DefinitionBuilder::new(def, world, uid)), None => Err(DefinitionErrorKind::NoSuchDefinition(uid.to_owned())), } } pub fn lookup_template(&self, uid: &str, component: &str) -> Option<&dyn Any> { - self.map + self.0 .get(uid) .and_then(|def| def.find_component(component)) } @@ -69,7 +63,7 @@ mod tests { #[test] fn duplicates() { - let mut reg = RegistryBuilder::new(); + let mut reg = DefinitionRegistryBuilder::new(); assert!(reg.register("nice".to_owned(), Definition::dummy()).is_ok()); assert!(reg diff --git a/game/simulation/src/ecs/world.rs b/game/simulation/src/ecs/world.rs index cb957074..b474eac7 100644 --- a/game/simulation/src/ecs/world.rs +++ b/game/simulation/src/ecs/world.rs @@ -8,12 +8,12 @@ use common::*; use crate::event::{DeathReason, EntityEvent, EntityEventQueue}; -use crate::definitions::{DefinitionBuilder, DefinitionErrorKind}; +use crate::definitions::{DefinitionBuilder, DefinitionErrorKind, DefinitionRegistry}; use crate::ecs::component::{AsInteractiveFn, ComponentRegistry}; use crate::ecs::*; use crate::item::{ContainerComponent, ContainerResolver}; -use crate::{definitions, Entity, InnerWorldRef, ItemStackComponent, TransformComponent, WorldRef}; +use crate::{Entity, InnerWorldRef, ItemStackComponent, TransformComponent, WorldRef}; use specs::prelude::Resource; use specs::world::EntitiesRes; @@ -338,7 +338,7 @@ impl ComponentWorld for EcsWorld { &self, definition_uid: &str, ) -> Result, DefinitionErrorKind> { - let definitions = self.resource::(); + let definitions = self.resource::(); definitions.instantiate(&*definition_uid, self) } diff --git a/game/simulation/src/society/job/jobs/build.rs b/game/simulation/src/society/job/jobs/build.rs index d49e2e85..c3843fe8 100644 --- a/game/simulation/src/society/job/jobs/build.rs +++ b/game/simulation/src/society/job/jobs/build.rs @@ -1,7 +1,7 @@ use crate::build::{ Build, BuildMaterial, ConsumedMaterialForJobComponent, ReservedMaterialComponent, }; -use crate::definitions::{DefinitionNameComponent, Registry}; +use crate::definitions::{DefinitionNameComponent, DefinitionRegistry}; use crate::ecs::{EcsWorld, Join, WorldExt}; use crate::item::HaulableItemComponent; use crate::job::job::{CompletedTasks, SocietyJobImpl}; @@ -289,7 +289,7 @@ impl SocietyJobImpl for BuildThingJob { // maybe that should be at a higher level than this // preprocess materials to get hands needed for hauling - let definitions = world.resource::(); + let definitions = world.resource::(); for mat in self.required_materials.iter() { let hands = match definitions.lookup_template(mat.definition(), "haulable") { Some(any) => { From a1689017a8c4180d9871ddcf06f5aa2e90f3d388 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Tue, 18 Jan 2022 12:12:43 +0200 Subject: [PATCH 12/46] Replace enum-iterator dep with strum --- Cargo.lock | 27 +++---------------- game/procgen/Cargo.toml | 3 +-- game/procgen/src/params.rs | 2 +- game/procgen/src/region/regions.rs | 2 +- game/simulation/Cargo.toml | 3 +-- game/simulation/src/build/registry/mod.rs | 0 game/simulation/src/event/subscription.rs | 3 ++- game/simulation/src/lib.rs | 3 ++- game/simulation/src/simulation.rs | 2 +- game/world/Cargo.toml | 2 +- game/world/src/block.rs | 4 +-- .../src/render/sdl/ui/windows/selection.rs | 6 ++--- 12 files changed, 19 insertions(+), 38 deletions(-) create mode 100644 game/simulation/src/build/registry/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d4263610..3dc70266 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,26 +812,6 @@ dependencies = [ "unit", ] -[[package]] -name = "enum-iterator" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79a6321a1197d7730510c7e3f6cb80432dfefecb32426de8cea0aa19b4bb8d7" -dependencies = [ - "enum-iterator-derive", -] - -[[package]] -name = "enum-iterator-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e94aa31f7c0dc764f57896dc615ddd76fc13b0d5dca7eb6cc5e018a5a09ec06" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "filetime" version = "0.2.14" @@ -2194,7 +2174,6 @@ dependencies = [ "spiral", "structopt", "strum", - "strum_macros", "tokio", "unit", ] @@ -2709,7 +2688,6 @@ dependencies = [ "specs", "specs-derive", "strum", - "strum_macros", "unit", "ux", "world", @@ -2875,6 +2853,9 @@ name = "strum" version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89a286a7e3b5720b9a477b23253bc50debac207c8d21505f8e70b36792f11b5" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" @@ -3291,13 +3272,13 @@ dependencies = [ "common", "config", "criterion", - "enum-iterator", "futures", "grid", "nd_iter", "num_cpus", "petgraph", "procgen", + "strum", "tokio", "unit", ] diff --git a/game/procgen/Cargo.toml b/game/procgen/Cargo.toml index aa6467d3..9d544d88 100644 --- a/game/procgen/Cargo.toml +++ b/game/procgen/Cargo.toml @@ -14,8 +14,7 @@ color = { path = "../../shared/color", optional = true } resources = { path = "../resources" } noise = { version = "0.6", default-features = false } # 0.7 changes noise parameters, avoid upgrade for now rand_distr = { version = "0.3" } -strum = "0.19" -strum_macros = "0.19" +strum = { version = "0.19", features = ["derive"] } tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "sync", "time"] } futures = { version = "0.3", default-features = false, features = ["std", "alloc", "async-await"] } geo = "0.16" diff --git a/game/procgen/src/params.rs b/game/procgen/src/params.rs index 342de322..59181a09 100644 --- a/game/procgen/src/params.rs +++ b/game/procgen/src/params.rs @@ -2,7 +2,7 @@ use common::*; use serde::Deserialize; use structopt::StructOpt; -use strum_macros::{EnumIter, EnumString}; +use strum::{EnumIter, EnumString}; use crate::biome::BiomeConfig; use crate::region::RegionLocationUnspecialized; diff --git a/game/procgen/src/region/regions.rs b/game/procgen/src/region/regions.rs index 6f4b15d3..36febdea 100644 --- a/game/procgen/src/region/regions.rs +++ b/game/procgen/src/region/regions.rs @@ -25,7 +25,7 @@ use std::hint::unreachable_unchecked; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, Ordering}; -use strum_macros::EnumDiscriminants; +use strum::EnumDiscriminants; pub struct Regions { params: PlanetParamsRef, diff --git a/game/simulation/Cargo.toml b/game/simulation/Cargo.toml index f5446d5a..405c44a2 100644 --- a/game/simulation/Cargo.toml +++ b/game/simulation/Cargo.toml @@ -25,8 +25,7 @@ arraydeque = "0.4" ahash = "0.7" inventory = "0.1" daggy = "0.7" -strum = "0.19" -strum_macros = "0.19" +strum = { version = "0.19", features = ["derive"] } bitflags = "1.2" sortedvec = "0.5" rbl_circular_buffer = "0.1" diff --git a/game/simulation/src/build/registry/mod.rs b/game/simulation/src/build/registry/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/game/simulation/src/event/subscription.rs b/game/simulation/src/event/subscription.rs index 0631dac6..cc1aa96c 100644 --- a/game/simulation/src/event/subscription.rs +++ b/game/simulation/src/event/subscription.rs @@ -4,8 +4,9 @@ use crate::ecs::*; use crate::needs::FoodEatingError; use crate::path::PathToken; use common::{num_derive::FromPrimitive, num_traits, Display}; +use num_traits::FromPrimitive; use std::convert::TryInto; -use strum_macros::EnumDiscriminants; +use strum::EnumDiscriminants; use unit::world::WorldPoint; use world::NavigationError; diff --git a/game/simulation/src/lib.rs b/game/simulation/src/lib.rs index d36298a1..f106902c 100644 --- a/game/simulation/src/lib.rs +++ b/game/simulation/src/lib.rs @@ -3,7 +3,7 @@ // Exports from world so the renderer only needs to link against simulation pub use world::{ - block::{BlockType, IntoEnumIterator}, + block::BlockType, loader::{ AsyncWorkerPool, BlockForAllError, TerrainSourceError, TerrainUpdatesRes, WorldLoader, WorldTerrainUpdate, @@ -59,6 +59,7 @@ pub use perf::{Perf, PerfAvg, Timing}; pub use queued_update::QueuedUpdates; pub use runtime::Runtime; pub use society::{job, NameGeneration, PlayerSociety, Societies, SocietyComponent, SocietyHandle}; +pub use strum::IntoEnumIterator; pub use unit::world::{ all_slabs_in_range, BlockPosition, ChunkLocation, SlabLocation, WorldPosition, WorldPositionRange, diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index 28f3725d..3e42258d 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -2,7 +2,7 @@ use std::ops::{Add, Deref}; use common::*; use resources::Resources; -use strum_macros::EnumDiscriminants; +use strum::EnumDiscriminants; use unit::world::{WorldPosition, WorldPositionRange}; use world::block::BlockType; use world::loader::{TerrainUpdatesRes, WorldTerrainUpdate}; diff --git a/game/world/Cargo.toml b/game/world/Cargo.toml index 04ec3a7d..2a34cb91 100644 --- a/game/world/Cargo.toml +++ b/game/world/Cargo.toml @@ -16,7 +16,7 @@ procgen = { path = "../procgen", default-features = false, features = ["cache"], petgraph = "0.5" nd_iter = "0.0" -enum-iterator = "0.6" +strum = { version = "0.19", features = ["derive"] } futures = { version = "0.3", default-features = false, features = ["std", "executor"] } tokio = { version = "1.0", default-features = false, features = ["time", "rt", "rt-multi-thread", "sync"] } diff --git a/game/world/src/block.rs b/game/world/src/block.rs index 91e63e9f..c0b9b11c 100644 --- a/game/world/src/block.rs +++ b/game/world/src/block.rs @@ -4,7 +4,7 @@ use crate::navigation::{ChunkArea, SlabAreaIndex}; use crate::occlusion::BlockOcclusion; use common::derive_more::Display; use common::Proportion; -pub use enum_iterator::IntoEnumIterator; +use strum::{EnumIter, EnumString}; use unit::world::GlobalSliceIndex; /// A single block in a chunk @@ -26,7 +26,7 @@ pub type BlockDurability = u8; // TODO define block types in data instead of code /// The type of a block -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, IntoEnumIterator, Display)] +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, EnumIter, Display)] pub enum BlockType { Air, Dirt, diff --git a/renderer/engine/src/render/sdl/ui/windows/selection.rs b/renderer/engine/src/render/sdl/ui/windows/selection.rs index 9a7d883e..60c92fc0 100644 --- a/renderer/engine/src/render/sdl/ui/windows/selection.rs +++ b/renderer/engine/src/render/sdl/ui/windows/selection.rs @@ -979,11 +979,11 @@ impl SelectionWindow { } if let Some(placement) = placement { - if let Some(bt) = BlockType::into_enum_iter().nth(self.edit_selection) { + if let Some(bt) = BlockType::iter().nth(self.edit_selection) { context.issue_request(UiRequest::FillSelectedTiles(placement, bt)); } else { // reset to a valid one - debug_assert!(BlockType::into_enum_iter().count() > 0); + debug_assert!(BlockType::iter().count() > 0); self.edit_selection = 0; } } @@ -995,7 +995,7 @@ impl SelectionWindow { .horizontal_scrollbar(true) .movable(false) .build(context.ui(), || { - for (i, ty) in BlockType::into_enum_iter().enumerate() { + for (i, ty) in BlockType::iter().enumerate() { if Selectable::new(ui_str!(in context, "{}", ty)) .selected(self.edit_selection == i) .build(context) From 4ada8441aff93932d01310cc2021628bc3e4a211 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Tue, 18 Jan 2022 13:16:26 +0200 Subject: [PATCH 13/46] Add string interning for definition names --- Cargo.lock | 13 +++ TODOS.md | 10 ++- game/simulation/Cargo.toml | 1 + game/simulation/src/build/builds.rs | 2 +- game/simulation/src/build/material.rs | 19 +++- game/simulation/src/definitions/builder.rs | 11 ++- game/simulation/src/definitions/component.rs | 3 +- .../simulation/src/definitions/loader/load.rs | 15 ++-- game/simulation/src/definitions/loader/mod.rs | 2 +- .../loader/step1_deserialization.rs | 5 +- .../definitions/loader/step3_construction.rs | 32 ++++--- game/simulation/src/definitions/registry.rs | 42 +++++---- game/simulation/src/item/filter.rs | 3 +- game/simulation/src/item/stack.rs | 7 +- game/simulation/src/lib.rs | 1 + game/simulation/src/simulation.rs | 6 +- game/simulation/src/society/job/jobs/build.rs | 28 +++--- game/simulation/src/string.rs | 87 +++++++++++++++++++ 18 files changed, 218 insertions(+), 69 deletions(-) create mode 100644 game/simulation/src/string.rs diff --git a/Cargo.lock b/Cargo.lock index 3dc70266..3d182caf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2689,6 +2689,7 @@ dependencies = [ "specs-derive", "strum", "unit", + "ustr", "ux", "world", ] @@ -3110,6 +3111,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "ustr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd539d8973e229b9d04f15d36e6a8f8d8f85f946b366f06bb001aaed3fa9dd9" +dependencies = [ + "ahash 0.7.2", + "byteorder", + "lazy_static", + "parking_lot", +] + [[package]] name = "ux" version = "0.1.3" diff --git a/TODOS.md b/TODOS.md index 00d390df..6e6220ba 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,4 +1,4 @@ -# TODOs (405) +# TODOs (409) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) @@ -188,8 +188,9 @@ * `// TODO collect jobs from society directly, which can filter them from the applicable work items too` * [game/simulation/src/build/builds.rs](game/simulation/src/build/builds.rs) (1) * `// TODO can this somehow return an iterator of build materials?` - * [game/simulation/src/build/material.rs](game/simulation/src/build/material.rs) (1) + * [game/simulation/src/build/material.rs](game/simulation/src/build/material.rs) (2) * `// TODO flexible list of reqs based on components` + * `// TODO remove this` * [game/simulation/src/build/world_helper.rs](game/simulation/src/build/world_helper.rs) (1) * `// TODO consume materials incrementally as progress is made` * [game/simulation/src/definitions/builder.rs](game/simulation/src/definitions/builder.rs) (1) @@ -200,6 +201,8 @@ * `// TODO remove abstract definitions` * [game/simulation/src/definitions/loader/mod.rs](game/simulation/src/definitions/loader/mod.rs) (1) * `// TODO consider using `nested` vecs as an optimization` + * [game/simulation/src/definitions/loader/step3_construction.rs](game/simulation/src/definitions/loader/step3_construction.rs) (1) + * `// TODO CachedStr for component names` * [game/simulation/src/definitions/mod.rs](game/simulation/src/definitions/mod.rs) (1) * `// TODO include which key caused the problem` * [game/simulation/src/ecs/component.rs](game/simulation/src/ecs/component.rs) (3) @@ -378,6 +381,9 @@ * `// TODO show actual steering direction alongside velocity` * [game/simulation/src/steer/system.rs](game/simulation/src/steer/system.rs) (1) * `// TODO cache allocation in system` + * [game/simulation/src/string.rs](game/simulation/src/string.rs) (2) + * `// TODO remove this` + * `// TODO report panic when cache is empty` * [game/world/src/block.rs](game/world/src/block.rs) (5) * `// TODO store sparse block data in the slab instead of inline in the block` * `// TODO define block types in data instead of code` diff --git a/game/simulation/Cargo.toml b/game/simulation/Cargo.toml index 405c44a2..63d4b44f 100644 --- a/game/simulation/Cargo.toml +++ b/game/simulation/Cargo.toml @@ -32,6 +32,7 @@ rbl_circular_buffer = "0.1" futures = "0.3" cooked-waker = "5.0" async-trait = "0.1" +ustr = "0.8" ron = "0.6" serde = "1.0" diff --git a/game/simulation/src/build/builds.rs b/game/simulation/src/build/builds.rs index 23798dc9..0a44be62 100644 --- a/game/simulation/src/build/builds.rs +++ b/game/simulation/src/build/builds.rs @@ -33,7 +33,7 @@ impl Build for StoneBrickWall { } fn materials(&self, materials_out: &mut Vec) { - materials_out.push(BuildMaterial::new( + materials_out.push(BuildMaterial::new_str( "core_brick_stone", NonZeroU16::new(6).unwrap(), )) diff --git a/game/simulation/src/build/material.rs b/game/simulation/src/build/material.rs index d09aa76f..476b116b 100644 --- a/game/simulation/src/build/material.rs +++ b/game/simulation/src/build/material.rs @@ -1,12 +1,13 @@ use crate::ecs::*; use crate::job::SocietyJobHandle; +use crate::string::{CachedStr, StringCache}; use std::fmt::{Debug, Formatter}; use std::num::NonZeroU16; #[derive(Hash, Clone, Eq, PartialEq)] pub struct BuildMaterial { // TODO flexible list of reqs based on components - definition_name: &'static str, + definition_name: CachedStr, quantity: NonZeroU16, } @@ -27,14 +28,24 @@ pub struct ReservedMaterialComponent { pub struct ConsumedMaterialForJobComponent; impl BuildMaterial { - pub fn new(definition_name: &'static str, quantity: NonZeroU16) -> Self { + /// Cheap + pub fn new(definition_name: CachedStr, quantity: NonZeroU16) -> Self { Self { definition_name, quantity, } } - pub fn definition(&self) -> &'static str { + // TODO remove this + #[deprecated] + pub fn new_str(definition_name: &str, quantity: NonZeroU16) -> Self { + Self { + definition_name: StringCache::get_temporary(definition_name), + quantity, + } + } + + pub fn definition(&self) -> CachedStr { self.definition_name } @@ -45,6 +56,6 @@ impl BuildMaterial { impl Debug for BuildMaterial { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}x{}", self.quantity, self.definition_name) + write!(f, "{}x{}", self.quantity, self.definition_name.as_ref()) } } diff --git a/game/simulation/src/definitions/builder.rs b/game/simulation/src/definitions/builder.rs index 94190127..3f574e51 100644 --- a/game/simulation/src/definitions/builder.rs +++ b/game/simulation/src/definitions/builder.rs @@ -3,6 +3,7 @@ use unit::world::WorldPosition; use crate::definitions::loader::Definition; use crate::definitions::DefinitionNameComponent; use crate::ecs::*; +use crate::string::{CachedStr, StringCache}; use crate::{ComponentWorld, InnerWorldRef, TransformComponent}; use common::*; @@ -27,22 +28,26 @@ pub enum BuilderError { pub struct DefinitionBuilder<'d, W: ComponentWorld> { definition: &'d Definition, /// Unnecessary when the temporary DefinitionNameComponent is removed - uid: String, + uid: CachedStr, world: &'d W, position: Option>, } impl<'d, W: ComponentWorld> DefinitionBuilder<'d, W> { - pub fn new(definition: &'d Definition, world: &'d W, uid: &str) -> Self { + pub fn new_with_cached(definition: &'d Definition, world: &'d W, uid: CachedStr) -> Self { Self { definition, world, - uid: uid.to_owned(), + uid, position: None, } } + pub fn new(definition: &'d Definition, world: &'d W, uid: &str) -> Self { + Self::new_with_cached(definition, world, world.resource::().get(uid)) + } + pub fn with_position(mut self, pos: P) -> Self { // TODO avoid box by resolving here and storing result self.position = Some(Box::new(pos)); diff --git a/game/simulation/src/definitions/component.rs b/game/simulation/src/definitions/component.rs index 8a506956..5168380f 100644 --- a/game/simulation/src/definitions/component.rs +++ b/game/simulation/src/definitions/component.rs @@ -1,4 +1,5 @@ use crate::ecs::*; +use crate::string::CachedStr; /// Terrible, inefficient and a disgusting way of identifying entities by their original definition /// name. This exists only for initial hacky entity comparisons for build job requirements, which @@ -8,4 +9,4 @@ use crate::ecs::*; #[derive(Component, EcsComponent, Clone, Debug)] #[storage(VecStorage)] #[name("original-definition")] -pub struct DefinitionNameComponent(pub String); +pub struct DefinitionNameComponent(pub CachedStr); diff --git a/game/simulation/src/definitions/loader/load.rs b/game/simulation/src/definitions/loader/load.rs index 1813f5da..1d66882a 100644 --- a/game/simulation/src/definitions/loader/load.rs +++ b/game/simulation/src/definitions/loader/load.rs @@ -1,22 +1,27 @@ use crate::definitions::loader::step1_deserialization::{ - collect_raw_definitions, DefinitionUid, DeserializedDefinition, + collect_raw_definitions, DeserializedDefinition, }; use crate::definitions::loader::step2_preprocessing::preprocess; use crate::definitions::loader::step3_construction::{instantiate, Definition}; use crate::definitions::loader::template_lookup::TemplateLookup; use crate::definitions::registry::DefinitionRegistryBuilder; use crate::definitions::{DefinitionError, DefinitionErrors, DefinitionRegistry}; +use crate::string::{CachedStr, StringCache}; -pub fn load(resources: resources::Definitions) -> Result { +pub fn load( + resources: resources::Definitions, + string_cache: &StringCache, +) -> Result { let defs = load_and_preprocess_with(|| collect_raw_definitions(resources))?; - let instantiated = instantiate(defs, &TemplateLookup::init())?; + let instantiated = instantiate(defs, &TemplateLookup::init(), string_cache)?; build_registry(instantiated) } #[cfg(test)] pub fn load_from_str(definitions: &str) -> Result { let defs = preprocess_from_str(definitions)?; - let instantiated = instantiate(defs, &TemplateLookup::init())?; + let string_cache = StringCache::default(); // cache is not cleared in tests on drop + let instantiated = instantiate(defs, &TemplateLookup::init(), &string_cache)?; build_registry(instantiated) } @@ -57,7 +62,7 @@ pub fn load_and_preprocess_with< } pub fn build_registry( - defs: Vec<(DefinitionUid, Definition)>, + defs: Vec<(CachedStr, Definition)>, ) -> Result { let mut errors = Vec::new(); diff --git a/game/simulation/src/definitions/loader/mod.rs b/game/simulation/src/definitions/loader/mod.rs index 52702703..a6383cd6 100644 --- a/game/simulation/src/definitions/loader/mod.rs +++ b/game/simulation/src/definitions/loader/mod.rs @@ -1,5 +1,5 @@ pub use load::load; -pub use step1_deserialization::{DefinitionSource, DefinitionUid}; +pub use step1_deserialization::DefinitionSource; pub use step3_construction::Definition; pub type ValueImpl = ron::Value; diff --git a/game/simulation/src/definitions/loader/step1_deserialization.rs b/game/simulation/src/definitions/loader/step1_deserialization.rs index a633a8ef..51e5e441 100644 --- a/game/simulation/src/definitions/loader/step1_deserialization.rs +++ b/game/simulation/src/definitions/loader/step1_deserialization.rs @@ -14,9 +14,8 @@ use crate::definitions::loader::step2_preprocessing::ProcessedComponents; use crate::definitions::{DefinitionError, DefinitionErrorKind}; use crate::ecs; use crate::ecs::ComponentBuildError; -use serde::de::Error; -pub type DefinitionUid = String; +use serde::de::Error; /// Parent hierarchy is unprocessed #[derive(Debug, Deserialize)] @@ -124,7 +123,7 @@ impl DeserializedDefinition { .map_err(|e| DefinitionError(source.clone(), DefinitionErrorKind::Format(e))) } - pub fn into_inner(self) -> (DefinitionUid, DefinitionSource, ProcessedComponents) { + pub fn into_inner(self) -> (String, DefinitionSource, ProcessedComponents) { ( self.uid, self.source, diff --git a/game/simulation/src/definitions/loader/step3_construction.rs b/game/simulation/src/definitions/loader/step3_construction.rs index 6a482fd2..ed8b9392 100644 --- a/game/simulation/src/definitions/loader/step3_construction.rs +++ b/game/simulation/src/definitions/loader/step3_construction.rs @@ -1,37 +1,40 @@ use common::*; use std::any::Any; -use crate::definitions::loader::step1_deserialization::{ - DefinitionSource, DefinitionUid, DeserializedDefinition, -}; +use crate::definitions::loader::step1_deserialization::{DefinitionSource, DeserializedDefinition}; use crate::definitions::loader::step2_preprocessing::ComponentFields; use crate::definitions::loader::template_lookup::TemplateLookup; use crate::definitions::{DefinitionError, DefinitionErrorKind, DefinitionErrors, ValueImpl}; use crate::ecs; use crate::ecs::ComponentTemplate; +use crate::string::{CachedStr, StringCache}; #[derive(Debug)] pub struct Definition { source: DefinitionSource, + // TODO CachedStr for component names components: Vec<(String, Box>)>, } pub fn instantiate( defs: Vec, templates: &TemplateLookup, -) -> Result, DefinitionErrors> { + string_cache: &StringCache, +) -> Result, DefinitionErrors> { let mut errors = Vec::new(); // instantiate components let instantiated = defs .into_iter() - .filter_map(|def| match Definition::construct(def, templates) { - Err(e) => { - errors.push(e); - None - } - Ok(d) => Some(d), - }) + .filter_map( + |def| match Definition::construct(def, templates, &string_cache) { + Err(e) => { + errors.push(e); + None + } + Ok(d) => Some(d), + }, + ) .collect(); if !errors.is_empty() { @@ -62,7 +65,8 @@ impl Definition { fn construct( deserialized: DeserializedDefinition, templates: &TemplateLookup, - ) -> Result<(DefinitionUid, Definition), DefinitionError> { + string_cache: &StringCache, + ) -> Result<(CachedStr, Definition), DefinitionError> { let (uid, source, components) = deserialized.into_inner(); let do_construct = || { @@ -91,7 +95,9 @@ impl Definition { }; match do_construct() { - Ok((uid, components)) => Ok((uid, Definition { source, components })), + Ok((uid, components)) => { + Ok((string_cache.get(&uid), Definition { source, components })) + } Err(e) => Err(DefinitionError(source, e)), } } diff --git a/game/simulation/src/definitions/registry.rs b/game/simulation/src/definitions/registry.rs index 93c7a7cb..65ac87bf 100644 --- a/game/simulation/src/definitions/registry.rs +++ b/game/simulation/src/definitions/registry.rs @@ -2,28 +2,34 @@ use crate::definitions::builder::DefinitionBuilder; use crate::definitions::{Definition, DefinitionErrorKind}; use std::any::Any; -use crate::definitions::loader::DefinitionUid; +use crate::string::{CachedStr, CachedStringHasher, StringCache}; use crate::ComponentWorld; use common::*; use std::collections::HashMap; -pub struct DefinitionRegistry(HashMap); +pub struct DefinitionRegistry(HashMap); -pub struct DefinitionRegistryBuilder(HashMap); +pub struct DefinitionRegistryBuilder(HashMap); impl DefinitionRegistryBuilder { pub fn new() -> Self { - Self(HashMap::with_capacity(512)) + Self(HashMap::with_capacity_and_hasher( + 512, + CachedStringHasher::default(), + )) } pub fn register( &mut self, - uid: DefinitionUid, + uid: CachedStr, definition: Definition, ) -> Result<(), (Definition, DefinitionErrorKind)> { #[allow(clippy::map_entry)] if self.0.contains_key(&uid) { - Err((definition, DefinitionErrorKind::AlreadyRegistered(uid))) + Err(( + definition, + DefinitionErrorKind::AlreadyRegistered(uid.as_ref().to_owned()), + )) } else { self.0.insert(uid, definition); Ok(()) @@ -45,15 +51,18 @@ impl DefinitionRegistry { uid: &str, world: &'w W, ) -> Result, DefinitionErrorKind> { - match self.0.get(uid) { - Some(def) => Ok(DefinitionBuilder::new(def, world, uid)), - None => Err(DefinitionErrorKind::NoSuchDefinition(uid.to_owned())), + let uid = world.resource::().get(uid); + match self.0.get(&uid) { + Some(def) => Ok(DefinitionBuilder::new_with_cached(def, world, uid)), + None => Err(DefinitionErrorKind::NoSuchDefinition( + uid.as_ref().to_owned(), + )), } } - pub fn lookup_template(&self, uid: &str, component: &str) -> Option<&dyn Any> { + pub fn lookup_template(&self, uid: CachedStr, component: &str) -> Option<&dyn Any> { self.0 - .get(uid) + .get(&uid) .and_then(|def| def.find_component(component)) } } @@ -64,13 +73,8 @@ mod tests { #[test] fn duplicates() { let mut reg = DefinitionRegistryBuilder::new(); - - assert!(reg.register("nice".to_owned(), Definition::dummy()).is_ok()); - assert!(reg - .register("nice".to_owned(), Definition::dummy()) - .is_err()); // duplicate - assert!(reg - .register("nice2".to_owned(), Definition::dummy()) - .is_ok()); + assert!(reg.register("nice".into(), Definition::dummy()).is_ok()); + assert!(reg.register("nice".into(), Definition::dummy()).is_err()); // duplicate + assert!(reg.register("nice2".into(), Definition::dummy()).is_ok()); } } diff --git a/game/simulation/src/item/filter.rs b/game/simulation/src/item/filter.rs index b8b1f066..1e985d0b 100644 --- a/game/simulation/src/item/filter.rs +++ b/game/simulation/src/item/filter.rs @@ -1,5 +1,6 @@ use crate::definitions::DefinitionNameComponent; use crate::ecs::Entity; +use crate::string::CachedStr; use crate::ComponentWorld; use common::*; @@ -8,7 +9,7 @@ pub enum ItemFilter { SpecificEntity(Entity), Predicate(fn(Entity) -> bool), HasComponent(&'static str), - MatchesDefinition(&'static str), + MatchesDefinition(CachedStr), // TODO filters on other fields e.g. mass, size, condition, etc } diff --git a/game/simulation/src/item/stack.rs b/game/simulation/src/item/stack.rs index 5216ade6..a3320015 100644 --- a/game/simulation/src/item/stack.rs +++ b/game/simulation/src/item/stack.rs @@ -7,6 +7,7 @@ use common::*; use crate::definitions::DefinitionNameComponent; use crate::ecs::*; +use crate::string::CachedStr; use crate::PhysicalComponent; #[derive(Debug, Error, Eq, PartialEq, Clone)] @@ -82,7 +83,7 @@ pub struct ItemStack { #[derive(Debug)] pub struct StackHomogeneity { // TODO use a better way than hacky definition names - definition: String, + definition: CachedStr, phantom: PhantomData, } @@ -356,7 +357,7 @@ impl ItemStack { impl Clone for StackHomogeneity { fn clone(&self) -> Self { Self { - definition: self.definition.clone(), + definition: self.definition, phantom: PhantomData, } } @@ -420,7 +421,7 @@ impl World for EcsWorld { self.component::(e) .ok() .map(|comp| StackHomogeneity { - definition: comp.0.clone(), + definition: comp.0, phantom: PhantomData, }) } diff --git a/game/simulation/src/lib.rs b/game/simulation/src/lib.rs index f106902c..1b3830c3 100644 --- a/game/simulation/src/lib.rs +++ b/game/simulation/src/lib.rs @@ -114,5 +114,6 @@ mod simulation; mod society; mod spatial; mod steer; +mod string; mod transform; mod world_debug; diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index 3e42258d..dc079bba 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -37,6 +37,7 @@ use crate::scripting::ScriptingContext; use crate::society::{NameGeneration, PlayerSociety}; use crate::spatial::{Spatial, SpatialSystem}; use crate::steer::{SteeringDebugRenderer, SteeringSystem}; +use crate::string::StringCache; use crate::world_debug::FeatureBoundaryDebugRenderer; use crate::{ definitions, BackendData, EntityEvent, EntityEventPayload, EntityLoggingComponent, Exit, @@ -110,10 +111,12 @@ impl world::WorldContext for WorldContext { impl Simulation { /// world_loader should have had some slabs requested pub fn new(world_loader: ThreadedWorldLoader, resources: Resources) -> BoxedResult { + let string_cache = StringCache::default(); + // load entity definitions from file system let definitions = { let def_root = resources.definitions()?; - definitions::load(def_root)? + definitions::load(def_root, &string_cache)? }; let voxel_world = world_loader.world(); @@ -122,6 +125,7 @@ impl Simulation { let mut ecs_world = EcsWorld::new(); ecs_world.insert(voxel_world.clone()); ecs_world.insert(definitions); + ecs_world.insert(string_cache); register_resources(&mut ecs_world, resources)?; // get a self referential ecs world resource pointing to itself diff --git a/game/simulation/src/society/job/jobs/build.rs b/game/simulation/src/society/job/jobs/build.rs index c3843fe8..d7a1deda 100644 --- a/game/simulation/src/society/job/jobs/build.rs +++ b/game/simulation/src/society/job/jobs/build.rs @@ -13,6 +13,7 @@ use crate::{ use common::*; use specs::BitSet; +use crate::string::{CachedStr, CachedStringHasher}; use std::collections::{HashMap, HashSet}; use std::num::NonZeroU16; @@ -31,13 +32,13 @@ pub struct BuildThingJob { reserved_materials: HashSet, /// Cache of extra hands needed for hauling each material - hands_needed: HashMap<&'static str, u16>, + hands_needed: HashMap, /// Steps completed so far progress: u32, /// Gross temporary way of tracking remaining materials - materials_remaining: HashMap<&'static str, NonZeroU16>, + materials_remaining: HashMap, /// Set if any material types are invalid e.g. not haulable missing_any_requirements: bool, @@ -72,7 +73,7 @@ pub enum BuildThingError { MissingDefinition(Entity), #[error("Material '{0}' is not required")] - MaterialNotRequired(String), + MaterialNotRequired(CachedStr), } pub enum MaterialReservation { @@ -92,9 +93,9 @@ impl BuildThingJob { build: Box::new(build), progress: 0, required_materials: materials, - hands_needed: HashMap::with_capacity(count), + hands_needed: HashMap::with_capacity_and_hasher(count, CachedStringHasher::default()), reserved_materials: HashSet::new(), - materials_remaining: HashMap::new(), // replaced on each call + materials_remaining: HashMap::with_hasher(CachedStringHasher::default()), // replaced on each call missing_any_requirements: false, this_job: None, ui_element: None, @@ -102,7 +103,10 @@ impl BuildThingJob { } // TODO fewer temporary allocations - fn check_materials(&mut self, world: &EcsWorld) -> HashMap<&'static str, NonZeroU16> { + fn check_materials( + &mut self, + world: &EcsWorld, + ) -> HashMap { let this_job = self.this_job.unwrap(); // set before this is called let job_pos = self.position.centred(); @@ -158,7 +162,7 @@ impl BuildThingJob { let stacks = world.read_storage::(); for (e, def, stack_opt) in (&reservations_bitset, &def_names, stacks.maybe()).join() { let entry = remaining_materials - .get_mut(def.0.as_str()) + .get_mut(&def.0) .unwrap_or_else(|| panic!("invalid reservation {:?}", def.0)); // TODO ensure this doesn't happen, or just handle it properly @@ -195,8 +199,8 @@ impl BuildThingJob { let n_required_ref = self .materials_remaining - .get_mut(def.0.as_str()) - .ok_or_else(|| BuildThingError::MaterialNotRequired(def.0.clone()))?; + .get_mut(&def.0) + .ok_or(BuildThingError::MaterialNotRequired(def.0))?; let n_required = n_required_ref.get(); let n_actual = { @@ -230,7 +234,7 @@ impl BuildThingJob { n.get() } None => { - self.materials_remaining.remove(def.0.as_str()); + self.materials_remaining.remove(&def.0); 0 } }; @@ -312,7 +316,7 @@ impl SocietyJobImpl for BuildThingJob { // gather materials first out.extend(self.required_materials.iter().cloned().flat_map(|mat| { - let extra_hands = *self.hands_needed.get(mat.definition()).unwrap(); // just inserted + let extra_hands = *self.hands_needed.get(&mat.definition()).unwrap(); // just inserted Some(SocietyTask::GatherMaterials { target: self.position, @@ -355,7 +359,7 @@ impl SocietyJobImpl for BuildThingJob { tasks.clear(); let this_job = self.this_job.unwrap(); // set unconditionally for (def, count) in outstanding_requirements.iter() { - let extra_hands = *self.hands_needed.get(*def).unwrap(); // already inserted + let extra_hands = *self.hands_needed.get(def).unwrap(); // already inserted let task = SocietyTask::GatherMaterials { target: self.position, diff --git a/game/simulation/src/string.rs b/game/simulation/src/string.rs new file mode 100644 index 00000000..70d665e5 --- /dev/null +++ b/game/simulation/src/string.rs @@ -0,0 +1,87 @@ +use common::*; +use std::fmt::Write; +use std::hash::BuildHasherDefault; +use ustr::{IdentityHasher, Ustr}; + +/// String interning for definitions and component names. Inserted as a resource into the world +/// and clears the global ustr cache on drop. +#[derive(Default)] +pub struct StringCache; + +/// Interned string that lives as long as the owning simulation instance +#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct CachedStr(Ustr); + +pub type CachedStringHasher = BuildHasherDefault; + +impl StringCache { + pub fn get(&self, s: &str) -> CachedStr { + CachedStr(ustr::ustr(s)) + } + + // TODO remove this + pub fn get_temporary(s: &str) -> CachedStr { + CachedStr(ustr::ustr(s)) + } + + #[cfg(test)] + pub fn get_direct(s: &str) -> CachedStr { + CachedStr(ustr::ustr(s)) + } +} + +#[cfg(test)] +impl From<&str> for CachedStr { + fn from(s: &str) -> Self { + StringCache::get_direct(s) + } +} + +// dont bother clearing in tests +#[cfg(not(test))] +impl Drop for StringCache { + fn drop(&mut self) { + let bytes = ustr::total_allocated(); + let n = ustr::num_entries(); + debug!("freeing {n} strings ({bytes} bytes) in string cache"); + trace!("string cache: {:?}", self); + // saftey: this is only dropped when the world is destroyed, and the game is over + unsafe { + ustr::_clear_cache(); + } + } +} + +impl AsRef for CachedStr { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl Display for CachedStr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl Debug for CachedStr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.0, f) + } +} + +impl Debug for StringCache { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "StringCache(n={}, bytes={}, strings=", + ustr::num_entries(), + ustr::total_allocated() + )?; + // TODO report panic when cache is empty + if ustr::num_entries() > 0 { + f.debug_list().entries(ustr::string_cache_iter()).finish()?; + } + f.write_char(')') + } +} From aae755c42826ce55c15e9b0f49fe7a969ef002b6 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Wed, 19 Jan 2022 11:45:16 +0200 Subject: [PATCH 14/46] Define build templates in data instead of code --- .planning/active.md | 5 +- TODOS.md | 17 +--- game/ai/src/intelligence.rs | 4 +- game/procgen/src/region/regions.rs | 2 +- game/simulation/src/ai/system.rs | 9 +- game/simulation/src/build/builds.rs | 41 -------- game/simulation/src/build/material.rs | 16 +--- game/simulation/src/build/mod.rs | 4 +- game/simulation/src/build/registry/mod.rs | 0 game/simulation/src/build/template.rs | 95 +++++++++++++++++++ game/simulation/src/definitions/loader/mod.rs | 7 +- .../definitions/loader/step3_construction.rs | 23 ++++- .../src/definitions/loader/template_lookup.rs | 7 +- game/simulation/src/definitions/registry.rs | 22 ++++- game/simulation/src/ecs/name.rs | 31 +++--- game/simulation/src/ecs/template.rs | 10 +- game/simulation/src/ecs/world.rs | 72 ++++++++++---- game/simulation/src/event/subscription.rs | 1 - game/simulation/src/input/system.rs | 18 ++-- game/simulation/src/item/component.rs | 21 ++-- game/simulation/src/item/containers.rs | 9 +- game/simulation/src/item/haul.rs | 9 +- .../src/item/inventory/component.rs | 16 +++- game/simulation/src/item/stack.rs | 2 - game/simulation/src/lib.rs | 3 +- game/simulation/src/movement.rs | 9 +- game/simulation/src/needs/food.rs | 9 +- game/simulation/src/render/system.rs | 9 +- game/simulation/src/senses/system.rs | 9 +- game/simulation/src/simulation.rs | 3 +- game/simulation/src/society/job/job.rs | 14 +-- game/simulation/src/society/job/jobs/build.rs | 35 ++++--- game/simulation/src/string.rs | 10 +- game/simulation/src/transform.rs | 9 +- game/world/src/block.rs | 4 +- .../src/render/sdl/ui/windows/society.rs | 30 +++--- renderer/main/src/scenarios.rs | 15 +-- resources/definitions/builds/wall.ron | 18 ++++ testing/src/tests/build.rs | 54 +++++------ 39 files changed, 438 insertions(+), 234 deletions(-) delete mode 100644 game/simulation/src/build/builds.rs delete mode 100644 game/simulation/src/build/registry/mod.rs create mode 100644 game/simulation/src/build/template.rs create mode 100644 resources/definitions/builds/wall.ron diff --git a/.planning/active.md b/.planning/active.md index 4571cd35..c9c69fea 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -13,6 +13,9 @@ * [ ] can shrink/expand selection by 1 block * [ ] extend material reservation to include the specific materials in transit for a build, to avoid others considering hauling more when it's already on the way -* [ ] register builds in data +* [o] register builds in data + * [ ] validate build material definition exists? + * [X] cache build templates intead of iterating all each time +* [ ] used cache strings for component names too * [ ] central registry of all builds including a unique identifier to refer to it in ui and its requests diff --git a/TODOS.md b/TODOS.md index 6e6220ba..9568c664 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,4 +1,4 @@ -# TODOs (409) +# TODOs (403) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) @@ -186,11 +186,8 @@ * `// TODO dont return a new vec of boxes, have some dignity` * `let mut applicable_tasks = Vec::new(); // TODO reuse allocation` * `// TODO collect jobs from society directly, which can filter them from the applicable work items too` - * [game/simulation/src/build/builds.rs](game/simulation/src/build/builds.rs) (1) - * `// TODO can this somehow return an iterator of build materials?` - * [game/simulation/src/build/material.rs](game/simulation/src/build/material.rs) (2) + * [game/simulation/src/build/material.rs](game/simulation/src/build/material.rs) (1) * `// TODO flexible list of reqs based on components` - * `// TODO remove this` * [game/simulation/src/build/world_helper.rs](game/simulation/src/build/world_helper.rs) (1) * `// TODO consume materials incrementally as progress is made` * [game/simulation/src/definitions/builder.rs](game/simulation/src/definitions/builder.rs) (1) @@ -333,10 +330,8 @@ * `// TODO sort out systems so they all have an ecs_world reference and can keep state` * `// TODO limit time/count` * `let discovered = empty(); // TODO include slabs discovered by members of player's society` - * [game/simulation/src/society/job/job.rs](game/simulation/src/society/job/job.rs) (3) + * [game/simulation/src/society/job/job.rs](game/simulation/src/society/job/job.rs) (1) * `/// TODO provide size hint that could be used as an optimisation for a small number of tasks (e.g. smallvec)` - * `// TODO specify which build` - * `// TODO use actual job provided` * [game/simulation/src/society/job/jobs/break_blocks.rs](game/simulation/src/society/job/jobs/break_blocks.rs) (1) * `// TODO add display impl for WorldPositionRange` * [game/simulation/src/society/job/jobs/build.rs](game/simulation/src/society/job/jobs/build.rs) (10) @@ -381,8 +376,7 @@ * `// TODO show actual steering direction alongside velocity` * [game/simulation/src/steer/system.rs](game/simulation/src/steer/system.rs) (1) * `// TODO cache allocation in system` - * [game/simulation/src/string.rs](game/simulation/src/string.rs) (2) - * `// TODO remove this` + * [game/simulation/src/string.rs](game/simulation/src/string.rs) (1) * `// TODO report panic when cache is empty` * [game/world/src/block.rs](game/world/src/block.rs) (5) * `// TODO store sparse block data in the slab instead of inline in the block` @@ -511,9 +505,8 @@ * `// TODO use the arena for this` * `// TODO other job tabs` * `// TODO proper way of checking if an entity is living` - * [renderer/engine/src/render/sdl/ui/windows/society.rs](renderer/engine/src/render/sdl/ui/windows/society.rs) (4) + * [renderer/engine/src/render/sdl/ui/windows/society.rs](renderer/engine/src/render/sdl/ui/windows/society.rs) (3) * `// TODO handle failure better?` - * `// TODO temporary hardcoded list of builds` * `// TODO preserve finished jobs and tasks for a bit and display them in the ui too` * `// TODO use table API when available` * [renderer/main/src/main.rs](renderer/main/src/main.rs) (5) diff --git a/game/ai/src/intelligence.rs b/game/ai/src/intelligence.rs index f5f83f9f..3624c3c5 100644 --- a/game/ai/src/intelligence.rs +++ b/game/ai/src/intelligence.rs @@ -286,8 +286,8 @@ mod tests { use crate::decision::WeightedDse; use crate::test_utils::*; use crate::{ - AiBox, Consideration, Considerations, DecisionSource, DecisionWeightType, Dse, - Intelligence, IntelligentDecision, + AiBox, Considerations, DecisionSource, DecisionWeightType, Dse, Intelligence, + IntelligentDecision, }; use common::{bumpalo, once}; diff --git a/game/procgen/src/region/regions.rs b/game/procgen/src/region/regions.rs index 36febdea..f01d99ef 100644 --- a/game/procgen/src/region/regions.rs +++ b/game/procgen/src/region/regions.rs @@ -1100,7 +1100,7 @@ mod tests { timeout_task.abort(); // generic checks - for ([x, y, _], entry) in regions.region_grid.iter_coords() { + for (_, entry) in regions.region_grid.iter_coords() { let guard = entry .0 .try_read() diff --git a/game/simulation/src/ai/system.rs b/game/simulation/src/ai/system.rs index bd3d780b..ab8261cd 100644 --- a/game/simulation/src/ai/system.rs +++ b/game/simulation/src/ai/system.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::iter::once; +use std::rc::Rc; use ai::{AiBox, DecisionSource, Dse, Intelligence, IntelligentDecision}; use common::*; @@ -17,6 +18,7 @@ use crate::{EntityLoggingComponent, TransformComponent}; use crate::alloc::FrameAllocator; use crate::job::JobIndex; +use crate::string::StringCache; use crate::{dse, Societies}; #[derive(Component, EcsComponent)] @@ -287,7 +289,10 @@ pub struct IntelligenceComponentTemplate { } impl ComponentTemplate for IntelligenceComponentTemplate { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { @@ -303,7 +308,7 @@ impl ComponentTemplate for IntelligenceComponentTemplate { } }; - Ok(Box::new(Self { species })) + Ok(Rc::new(Self { species })) } fn instantiate<'b>(&self, builder: EntityBuilder<'b>) -> EntityBuilder<'b> { diff --git a/game/simulation/src/build/builds.rs b/game/simulation/src/build/builds.rs deleted file mode 100644 index 0a44be62..00000000 --- a/game/simulation/src/build/builds.rs +++ /dev/null @@ -1,41 +0,0 @@ -// these will be defined in data at some point - -use crate::build::material::BuildMaterial; -use common::Display; -use std::fmt; -use std::num::NonZeroU16; -use world::block::BlockType; - -pub trait Build: fmt::Debug + fmt::Display { - /// Target block - fn output(&self) -> BlockType; - - /// (number of steps required, ticks to sleep between each step) - fn progression(&self) -> (u32, u32); - - // TODO can this somehow return an iterator of build materials? - fn materials(&self, materials_out: &mut Vec); -} - -// ------- - -/// Stone brick wall -#[derive(Debug, Display)] -pub struct StoneBrickWall; - -impl Build for StoneBrickWall { - fn output(&self) -> BlockType { - BlockType::StoneBrickWall - } - - fn progression(&self) -> (u32, u32) { - (10, 4) - } - - fn materials(&self, materials_out: &mut Vec) { - materials_out.push(BuildMaterial::new_str( - "core_brick_stone", - NonZeroU16::new(6).unwrap(), - )) - } -} diff --git a/game/simulation/src/build/material.rs b/game/simulation/src/build/material.rs index 476b116b..dfc25e68 100644 --- a/game/simulation/src/build/material.rs +++ b/game/simulation/src/build/material.rs @@ -1,9 +1,10 @@ -use crate::ecs::*; -use crate::job::SocietyJobHandle; -use crate::string::{CachedStr, StringCache}; use std::fmt::{Debug, Formatter}; use std::num::NonZeroU16; +use crate::ecs::*; +use crate::job::SocietyJobHandle; +use crate::string::CachedStr; + #[derive(Hash, Clone, Eq, PartialEq)] pub struct BuildMaterial { // TODO flexible list of reqs based on components @@ -36,15 +37,6 @@ impl BuildMaterial { } } - // TODO remove this - #[deprecated] - pub fn new_str(definition_name: &str, quantity: NonZeroU16) -> Self { - Self { - definition_name: StringCache::get_temporary(definition_name), - quantity, - } - } - pub fn definition(&self) -> CachedStr { self.definition_name } diff --git a/game/simulation/src/build/mod.rs b/game/simulation/src/build/mod.rs index a0344b36..d8b31c05 100644 --- a/game/simulation/src/build/mod.rs +++ b/game/simulation/src/build/mod.rs @@ -1,6 +1,6 @@ -mod builds; mod material; +mod template; mod world_helper; -pub use builds::*; pub use material::{BuildMaterial, ConsumedMaterialForJobComponent, ReservedMaterialComponent}; +pub use template::BuildTemplate; diff --git a/game/simulation/src/build/registry/mod.rs b/game/simulation/src/build/registry/mod.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/game/simulation/src/build/template.rs b/game/simulation/src/build/template.rs new file mode 100644 index 00000000..acb22619 --- /dev/null +++ b/game/simulation/src/build/template.rs @@ -0,0 +1,95 @@ +use std::num::NonZeroU16; +use std::rc::Rc; + +use serde::Deserialize; + +use world::block::BlockType; + +use crate::ecs::*; +use crate::string::StringCache; +use crate::BuildMaterial; + +#[derive(Debug)] +pub struct BuildTemplate { + materials: Vec, + steps: u32, + rate: u32, + output: BlockType, +} + +impl BuildTemplate { + pub const fn output(&self) -> BlockType { + self.output + } + + /// (number of steps required, ticks to sleep between each step) + pub const fn progression(&self) -> (u32, u32) { + (self.steps, self.rate) + } + + pub fn materials(&self) -> &[BuildMaterial] { + &self.materials + } + + #[cfg(any(test, feature = "testing"))] + pub fn new(materials: Vec, steps: u32, rate: u32, output: BlockType) -> Self { + Self { + materials, + steps, + rate, + output, + } + } +} + +impl ComponentTemplate for BuildTemplate { + fn construct( + values: &mut Map, + string_cache: &StringCache, + ) -> Result>, ComponentBuildError> + where + Self: Sized, + { + #[derive(Deserialize, Debug)] + struct Material(String, u16); + + let materials = { + let raw: Vec = values.get("materials").and_then(|val| val.into_type())?; + let mut materials = Vec::with_capacity(raw.len()); + for mat in raw { + let n = NonZeroU16::new(mat.1).ok_or_else(|| { + ComponentBuildError::TemplateSpecific(format!( + "material count for {:?} cannot be zero", + mat.0 + )) + })?; + materials.push(BuildMaterial::new(string_cache.get(&mat.0), n)) + } + materials + }; + + let steps = values.get_int("steps")?; + let rate = values.get_int("rate")?; + let output = { + let name = values.get_string("output")?; + name.parse::().map_err(|_| { + ComponentBuildError::TemplateSpecific(format!("invalid block type {:?}", name)) + })? + }; + + Ok(Rc::new(BuildTemplate { + materials, + steps, + rate, + output, + })) + } + + fn instantiate<'b>(&self, builder: EntityBuilder<'b>) -> EntityBuilder<'b> { + builder + } + + crate::as_any!(); +} + +register_component_template!("build", BuildTemplate); diff --git a/game/simulation/src/definitions/loader/mod.rs b/game/simulation/src/definitions/loader/mod.rs index a6383cd6..77e558ec 100644 --- a/game/simulation/src/definitions/loader/mod.rs +++ b/game/simulation/src/definitions/loader/mod.rs @@ -21,6 +21,8 @@ mod tests { use crate::definitions::loader::step1_deserialization::DeserializedDefinition; use crate::definitions::DefinitionErrorKind; use crate::ecs::*; + use crate::string::StringCache; + use std::rc::Rc; use super::*; @@ -52,11 +54,12 @@ mod tests { impl ComponentTemplate for TestComponentTemplate { fn construct( values: &mut Map, - ) -> Result>, ComponentBuildError> + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { - Ok(Box::new(Self { + Ok(Rc::new(Self { int: values.get_int("int")?, string: values.get_string("string")?, })) diff --git a/game/simulation/src/definitions/loader/step3_construction.rs b/game/simulation/src/definitions/loader/step3_construction.rs index ed8b9392..b5f27ca8 100644 --- a/game/simulation/src/definitions/loader/step3_construction.rs +++ b/game/simulation/src/definitions/loader/step3_construction.rs @@ -1,5 +1,7 @@ -use common::*; use std::any::Any; +use std::rc::Rc; + +use common::*; use crate::definitions::loader::step1_deserialization::{DefinitionSource, DeserializedDefinition}; use crate::definitions::loader::step2_preprocessing::ComponentFields; @@ -13,7 +15,7 @@ use crate::string::{CachedStr, StringCache}; pub struct Definition { source: DefinitionSource, // TODO CachedStr for component names - components: Vec<(String, Box>)>, + components: Vec<(String, Rc>)>, } pub fn instantiate( @@ -27,7 +29,7 @@ pub fn instantiate( let instantiated = defs .into_iter() .filter_map( - |def| match Definition::construct(def, templates, &string_cache) { + |def| match Definition::construct(def, templates, string_cache) { Err(e) => { errors.push(e); None @@ -58,6 +60,18 @@ impl Definition { }) } + pub fn find_component_ref(&self, name: &str) -> Option> { + self.components.iter().find_map(|(comp, template)| { + if name == comp && template.as_any().is::() { + let rc = template.clone(); + // safety: type has been checked + Some(unsafe { Rc::from_raw(Rc::into_raw(rc) as _) }) + } else { + None + } + }) + } + pub fn source(&self) -> DefinitionSource { self.source.clone() } @@ -79,7 +93,8 @@ impl Definition { ComponentFields::Negate => unimplemented!(), }; - let component_template = templates.construct(key.as_str(), &mut map)?; + let component_template = + templates.construct(key.as_str(), &mut map, string_cache)?; for leftover in map.keys() { warn!( diff --git a/game/simulation/src/definitions/loader/template_lookup.rs b/game/simulation/src/definitions/loader/template_lookup.rs index f353fed6..7077a9e9 100644 --- a/game/simulation/src/definitions/loader/template_lookup.rs +++ b/game/simulation/src/definitions/loader/template_lookup.rs @@ -1,10 +1,12 @@ use std::collections::HashMap; +use std::rc::Rc; use common::*; use crate::definitions::{DefinitionErrorKind, ValueImpl}; use crate::ecs; use crate::ecs::{ComponentTemplate, ComponentTemplateEntry}; +use crate::string::StringCache; /// Holds all registered component template entries pub struct TemplateLookup(HashMap<&'static str, ComponentTemplateEntry>); @@ -24,10 +26,11 @@ impl TemplateLookup { &self, uid: &str, map: &mut ecs::Map, - ) -> Result>, DefinitionErrorKind> { + string_cache: &StringCache, + ) -> Result>, DefinitionErrorKind> { self.0 .get(uid) .ok_or_else(|| DefinitionErrorKind::NoSuchComponent(uid.to_owned())) - .and_then(|e| (e.construct_fn)(map).map_err(DefinitionErrorKind::from)) + .and_then(|e| (e.construct_fn)(map, string_cache).map_err(DefinitionErrorKind::from)) } } diff --git a/game/simulation/src/definitions/registry.rs b/game/simulation/src/definitions/registry.rs index 65ac87bf..7774ef5f 100644 --- a/game/simulation/src/definitions/registry.rs +++ b/game/simulation/src/definitions/registry.rs @@ -1,6 +1,5 @@ use crate::definitions::builder::DefinitionBuilder; use crate::definitions::{Definition, DefinitionErrorKind}; -use std::any::Any; use crate::string::{CachedStr, CachedStringHasher, StringCache}; use crate::ComponentWorld; @@ -60,10 +59,23 @@ impl DefinitionRegistry { } } - pub fn lookup_template(&self, uid: CachedStr, component: &str) -> Option<&dyn Any> { - self.0 - .get(&uid) - .and_then(|def| def.find_component(component)) + pub fn lookup_definition(&self, uid: CachedStr) -> Option<&Definition> { + self.0.get(&uid) + } + + pub fn iter_templates( + &self, + component: &'static str, + ) -> impl Iterator + '_ { + self.0.iter().filter_map(move |(name, def)| { + def.find_component(component).and_then(|template| { + if template.is::() { + Some(*name) + } else { + None + } + }) + }) } } #[cfg(test)] diff --git a/game/simulation/src/ecs/name.rs b/game/simulation/src/ecs/name.rs index fafc2197..400a84e9 100644 --- a/game/simulation/src/ecs/name.rs +++ b/game/simulation/src/ecs/name.rs @@ -1,19 +1,23 @@ -use crate::ecs::*; -use crate::{ - ItemStackComponent, PlayerSociety, Societies, SocietyComponent, Tick, TransformComponent, - UiElementComponent, -}; +use crate::string::StringCache; +use std::collections::HashMap; +use std::fmt::{Display, Formatter, Write}; +use std::hint::unreachable_unchecked; +use std::rc::Rc; + +use specs::storage::StorageEntry; + +use ::world::block::BlockType; use common::*; use crate::build::ReservedMaterialComponent; +use crate::ecs::*; use crate::input::{MouseLocation, SelectedComponent}; use crate::job::BuildThingJob; use crate::spatial::{Spatial, Transforms}; -use ::world::block::BlockType; -use specs::storage::StorageEntry; -use std::collections::HashMap; -use std::fmt::{Display, Formatter, Write}; -use std::hint::unreachable_unchecked; +use crate::{ + ItemStackComponent, PlayerSociety, Societies, SocietyComponent, Tick, TransformComponent, + UiElementComponent, +}; // TODO smol string and/or cow and/or pool common strings @@ -351,11 +355,14 @@ impl Display for NameComponent { } impl ComponentTemplate for KindComponent { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { - Ok(Box::new(Self(values.get_string("singular")?, None))) + Ok(Rc::new(Self(values.get_string("singular")?, None))) } fn instantiate<'b>(&self, builder: EntityBuilder<'b>) -> EntityBuilder<'b> { diff --git a/game/simulation/src/ecs/template.rs b/game/simulation/src/ecs/template.rs index 415b1e5d..a12da6a6 100644 --- a/game/simulation/src/ecs/template.rs +++ b/game/simulation/src/ecs/template.rs @@ -1,10 +1,15 @@ pub use crate::definitions::ValueImpl; use crate::ecs::*; +use crate::string::StringCache; use common::Debug; use std::any::Any; +use std::rc::Rc; pub trait ComponentTemplate: Debug { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + string_cache: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized; @@ -16,7 +21,8 @@ pub trait ComponentTemplate: Debug { #[derive(Clone)] pub struct ComponentTemplateEntry { pub key: &'static str, - pub construct_fn: fn(&mut Map) -> Result>, ComponentBuildError>, + pub construct_fn: + fn(&mut Map, &StringCache) -> Result>, ComponentBuildError>, } inventory::collect!(ComponentTemplateEntry); diff --git a/game/simulation/src/ecs/world.rs b/game/simulation/src/ecs/world.rs index b474eac7..a9638860 100644 --- a/game/simulation/src/ecs/world.rs +++ b/game/simulation/src/ecs/world.rs @@ -1,30 +1,31 @@ -use specs::storage::InsertResult; - use std::any::TypeId; use std::hint::unreachable_unchecked; use std::mem::ManuallyDrop; +use std::ops::{Deref, DerefMut}; +use std::rc::Rc; -use common::*; +use specs::prelude::Resource; +use specs::storage::InsertResult; +use specs::world::EntitiesRes; +use specs::LazyUpdate; -use crate::event::{DeathReason, EntityEvent, EntityEventQueue}; +use common::*; +use crate::build::BuildTemplate; use crate::definitions::{DefinitionBuilder, DefinitionErrorKind, DefinitionRegistry}; use crate::ecs::component::{AsInteractiveFn, ComponentRegistry}; use crate::ecs::*; +use crate::event::{DeathReason, EntityEvent, EntityEventQueue}; use crate::item::{ContainerComponent, ContainerResolver}; - +use crate::string::CachedStr; use crate::{Entity, InnerWorldRef, ItemStackComponent, TransformComponent, WorldRef}; -use specs::prelude::Resource; -use specs::world::EntitiesRes; -use specs::LazyUpdate; - -use std::ops::{Deref, DerefMut}; - pub type SpecsWorld = specs::World; pub struct EcsWorld { world: SpecsWorld, component_registry: ComponentRegistry, + /// (definition name, build template, rendered KindComponent) + build_templates: Vec<(CachedStr, Rc, Option)>, } pub struct CachedWorldRef<'a> { @@ -221,15 +222,39 @@ impl DerefMut for EcsWorld { } impl EcsWorld { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { + pub fn with_definitions(definitions: DefinitionRegistry) -> Self { let mut world = SpecsWorld::new(); let reg = ComponentRegistry::new(&mut world); + let build_templates = definitions + .iter_templates::("build") + .filter_map(|def| { + let definition = definitions.lookup_definition(def)?; + + let build = definition.find_component_ref::("build")?; + + let name = definition + .find_component("kind") + .and_then(|any| any.downcast_ref::()) + .map(|kind| format!("{}", kind)); - EcsWorld { + Some((def, build, name)) + }) + .collect_vec(); + + let mut world = EcsWorld { world, component_registry: reg, - } + build_templates, + }; + + world.world.insert(definitions); + world + } + + #[cfg(test)] + pub fn new() -> Self { + let reg = crate::definitions::load_from_str("[]").expect("can't load null definitions"); + Self::with_definitions(reg) } /// Iterates through all known component types and checks each one @@ -251,6 +276,20 @@ impl EcsWorld { pub fn find_non_copyable(&self, entity: Entity) -> Option<&'static str> { self.component_registry.find_non_copyable(self, entity) } + + pub fn build_templates(&self) -> &[(CachedStr, Rc, Option)] { + &self.build_templates + } + + pub fn find_build_template(&self, name: &str) -> Option> { + self.build_templates.iter().find_map(|(def, template, _)| { + if def.as_ref() == name { + Some(template.clone()) + } else { + None + } + }) + } } impl ComponentWorld for EcsWorld { @@ -569,9 +608,10 @@ impl<'a> CachedWorldRef<'a> { #[cfg(test)] mod tests { - use super::*; use crate::ecs::*; + use super::*; + #[derive(Debug, Component, EcsComponent, Clone)] #[storage(VecStorage)] #[interactive] diff --git a/game/simulation/src/event/subscription.rs b/game/simulation/src/event/subscription.rs index cc1aa96c..e8d915aa 100644 --- a/game/simulation/src/event/subscription.rs +++ b/game/simulation/src/event/subscription.rs @@ -4,7 +4,6 @@ use crate::ecs::*; use crate::needs::FoodEatingError; use crate::path::PathToken; use common::{num_derive::FromPrimitive, num_traits, Display}; -use num_traits::FromPrimitive; use std::convert::TryInto; use strum::EnumDiscriminants; use unit::world::WorldPoint; diff --git a/game/simulation/src/input/system.rs b/game/simulation/src/input/system.rs index 9dc606f3..3f3b8305 100644 --- a/game/simulation/src/input/system.rs +++ b/game/simulation/src/input/system.rs @@ -153,14 +153,17 @@ impl SelectedEntity { } mod selected_tiles { - use super::*; - use crate::input::SelectionModification; - use crate::InnerWorldRef; use std::collections::BTreeMap; use std::fmt::Write; - use unit::world::{all_slabs_in_range, SlabLocation}; + + use unit::world::SlabLocation; use world::block::BlockType; + use crate::input::SelectionModification; + use crate::InnerWorldRef; + + use super::*; + #[derive(Default, Clone)] pub struct SelectedTiles { current: Option, @@ -269,9 +272,9 @@ mod selected_tiles { } pub fn on_world_change(&mut self, world: &WorldRef) { - self.current - .as_mut() - .map(|sel| sel.on_world_change(world.borrow())); + if let Some(sel) = self.current.as_mut() { + sel.on_world_change(world.borrow()) + } } } @@ -380,7 +383,6 @@ mod selected_tiles { mod tests { use super::*; use crate::input::system::selected_tiles::BlockOccurrences; - use std::collections::HashMap; use world::block::BlockType; #[test] diff --git a/game/simulation/src/item/component.rs b/game/simulation/src/item/component.rs index 6718fba6..2d772975 100644 --- a/game/simulation/src/item/component.rs +++ b/game/simulation/src/item/component.rs @@ -1,8 +1,10 @@ use common::derive_more::*; +use std::rc::Rc; use crate::ecs::*; use crate::item::condition::ItemCondition; use crate::needs::Fuel; +use crate::string::StringCache; /// Condition/durability of an entity, e.g. a tool or food #[derive(Component, EcsComponent, Constructor, Clone, Debug)] @@ -34,11 +36,14 @@ pub struct ThrowableItemComponent; // TODO weapon (damage to target per hit, damage to own condition per hit, attack speed, cooldown) impl ComponentTemplate for ConditionComponent { - fn construct(_: &mut Map) -> Result>, ComponentBuildError> + fn construct( + _: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { - Ok(Box::new(Self(ItemCondition::perfect()))) + Ok(Rc::new(Self(ItemCondition::perfect()))) } fn instantiate<'b>(&self, builder: EntityBuilder<'b>) -> EntityBuilder<'b> { @@ -51,8 +56,9 @@ impl ComponentTemplate for ConditionComponent { impl ComponentTemplate for EdibleItemComponent { fn construct( values: &mut Map, - ) -> Result>, ComponentBuildError> { - Ok(Box::new(Self { + _: &StringCache, + ) -> Result>, ComponentBuildError> { + Ok(Rc::new(Self { total_nutrition: values.get_int("total_nutrition")?, extra_hands: values.get_int("extra_hands")?, })) @@ -66,8 +72,11 @@ impl ComponentTemplate for EdibleItemComponent { } impl ComponentTemplate for ThrowableItemComponent { - fn construct(_: &mut Map) -> Result>, ComponentBuildError> { - Ok(Box::new(Self)) + fn construct( + _: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> { + Ok(Rc::new(Self)) } fn instantiate<'b>(&self, builder: EntityBuilder<'b>) -> EntityBuilder<'b> { diff --git a/game/simulation/src/item/containers.rs b/game/simulation/src/item/containers.rs index 018e7a74..a792bc67 100644 --- a/game/simulation/src/item/containers.rs +++ b/game/simulation/src/item/containers.rs @@ -1,5 +1,6 @@ use common::*; use std::num::NonZeroU16; +use std::rc::Rc; use unit::space::volume::Volume; use unit::world::WorldPosition; @@ -14,6 +15,7 @@ use crate::{ use crate::item::stack::{EntityCopyability, ItemStackComponent, StackAdd, StackMigrationType}; use crate::item::{ContainerComponent, ItemStack, ItemStackError}; use crate::simulation::AssociatedBlockData; +use crate::string::StringCache; #[derive(Debug, Error, Clone)] pub enum ContainersError { @@ -452,12 +454,15 @@ impl Display for ContainedInComponent { } impl ComponentTemplate for StackableComponent { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { let max_count = values.get_int("max_count")?; - Ok(Box::new(Self { max_count })) + Ok(Rc::new(Self { max_count })) } fn instantiate<'b>(&self, builder: EntityBuilder<'b>) -> EntityBuilder<'b> { diff --git a/game/simulation/src/item/haul.rs b/game/simulation/src/item/haul.rs index af8d4acc..a495046e 100644 --- a/game/simulation/src/item/haul.rs +++ b/game/simulation/src/item/haul.rs @@ -1,7 +1,9 @@ use common::*; +use std::rc::Rc; use unit::world::WorldPoint; use crate::ecs::*; +use crate::string::StringCache; use crate::TransformComponent; @@ -127,12 +129,15 @@ impl Default for EndHaulBehaviour { } impl ComponentTemplate for HaulableItemComponent { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { let extra_hands = values.get_int("extra_hands")?; - Ok(Box::new(HaulableItemComponent { extra_hands })) + Ok(Rc::new(HaulableItemComponent { extra_hands })) } fn instantiate<'b>(&self, builder: EntityBuilder<'b>) -> EntityBuilder<'b> { diff --git a/game/simulation/src/item/inventory/component.rs b/game/simulation/src/item/inventory/component.rs index b87c95f7..c8df00ba 100644 --- a/game/simulation/src/item/inventory/component.rs +++ b/game/simulation/src/item/inventory/component.rs @@ -1,6 +1,7 @@ use std::hint::unreachable_unchecked; use std::iter::repeat_with; use std::ops::{Deref, DerefMut, Range}; +use std::rc::Rc; use common::*; use unit::space::length::Length3; @@ -12,6 +13,7 @@ use crate::SocietyHandle; use crate::item::inventory::equip::EquipSlot; use crate::item::inventory::{Container, HeldEntity}; use crate::item::{ItemFilter, ItemFilterable}; +use crate::string::StringCache; /// Temporary dumb component to hold equip slots and containers. Will eventually be a view on top of /// the physical body tree @@ -665,12 +667,15 @@ impl ContainerComponent { } impl ComponentTemplate for InventoryComponentTemplate { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { let equip_slots = values.get_int("equip_slots")?; - Ok(Box::new(Self { equip_slots })) + Ok(Rc::new(Self { equip_slots })) } fn instantiate<'b>(&self, builder: EntityBuilder<'b>) -> EntityBuilder<'b> { @@ -689,14 +694,17 @@ struct SizeLimit { } impl ComponentTemplate for ContainerComponentTemplate { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { let volume: u16 = values.get_int("volume")?; let size: SizeLimit = values.get("size")?.into_type()?; - Ok(Box::new(ContainerComponentTemplate { + Ok(Rc::new(ContainerComponentTemplate { volume: Volume::new(volume), size: Length3::new(size.x, size.y, size.z), })) diff --git a/game/simulation/src/item/stack.rs b/game/simulation/src/item/stack.rs index a3320015..94051592 100644 --- a/game/simulation/src/item/stack.rs +++ b/game/simulation/src/item/stack.rs @@ -715,8 +715,6 @@ mod tests { #[test] fn homogeneity() { - let world = TestWorld::default(); - let mut odd_stack = TestStack::new(1, 10); assert!(matches!( odd_stack.add(2), diff --git a/game/simulation/src/lib.rs b/game/simulation/src/lib.rs index 1b3830c3..77827d7a 100644 --- a/game/simulation/src/lib.rs +++ b/game/simulation/src/lib.rs @@ -45,7 +45,7 @@ pub use event::{DeathReason, EntityEvent, EntityEventPayload}; #[cfg(feature = "testing")] pub use event::{EntityEventDebugPayload, TaskResultSummary}; -pub use build::{Build, BuildMaterial, StoneBrickWall}; +pub use build::{BuildMaterial, BuildTemplate}; #[cfg(debug_assertions)] pub use item::validation::validate_all_inventories; pub use item::{ @@ -59,6 +59,7 @@ pub use perf::{Perf, PerfAvg, Timing}; pub use queued_update::QueuedUpdates; pub use runtime::Runtime; pub use society::{job, NameGeneration, PlayerSociety, Societies, SocietyComponent, SocietyHandle}; +pub use string::{CachedStr, StringCache}; pub use strum::IntoEnumIterator; pub use unit::world::{ all_slabs_in_range, BlockPosition, ChunkLocation, SlabLocation, WorldPosition, diff --git a/game/simulation/src/movement.rs b/game/simulation/src/movement.rs index a6eea783..2fc8af5c 100644 --- a/game/simulation/src/movement.rs +++ b/game/simulation/src/movement.rs @@ -1,4 +1,5 @@ use common::*; +use std::rc::Rc; use crate::ecs::*; use crate::path::FollowPathComponent; @@ -6,6 +7,7 @@ use crate::steer::context::ContextMap; use crate::steer::SteeringComponent; use crate::physics::PhysicsComponent; +use crate::string::StringCache; /// Desired movement by the brain #[derive(Copy, Clone, Default, Component, EcsComponent)] @@ -57,11 +59,14 @@ pub struct MovementConfigComponent { } impl ComponentTemplate for MovementConfigComponent { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { - Ok(Box::new(Self { + Ok(Rc::new(Self { max_speed: values.get_float("max_speed")?, acceleration: values.get_float("acceleration")?, })) diff --git a/game/simulation/src/needs/food.rs b/game/simulation/src/needs/food.rs index 88dade27..bcb001dc 100644 --- a/game/simulation/src/needs/food.rs +++ b/game/simulation/src/needs/food.rs @@ -1,10 +1,12 @@ use common::newtype::AccumulativeInt; use common::*; +use std::rc::Rc; use crate::ecs::*; use crate::event::{EntityEvent, EntityEventPayload, EntityEventQueue}; use crate::item::{EdibleItemComponent, InventoryComponent}; use crate::simulation::EcsWorldRef; +use crate::string::StringCache; use crate::{ActivityComponent, ConditionComponent}; // TODO newtype for Fuel @@ -188,12 +190,15 @@ impl HungerComponent { } impl ComponentTemplate for HungerComponent { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { let max = values.get_int("max")?; - Ok(Box::new(Self::new(max))) + Ok(Rc::new(Self::new(max))) } fn instantiate<'b>(&self, builder: EntityBuilder<'b>) -> EntityBuilder<'b> { diff --git a/game/simulation/src/render/system.rs b/game/simulation/src/render/system.rs index cd831136..fc0ea424 100644 --- a/game/simulation/src/render/system.rs +++ b/game/simulation/src/render/system.rs @@ -1,4 +1,5 @@ use std::convert::TryInto; +use std::rc::Rc; use serde::de::Error; @@ -12,6 +13,7 @@ use crate::input::{SelectedComponent, SelectedTiles, SelectionProgress}; use crate::render::renderer::Renderer; use crate::render::shape::RenderHexColor; use crate::render::UiElementComponent; +use crate::string::StringCache; use crate::transform::{PhysicalComponent, TransformRenderDescription}; use crate::{PlayerSociety, Shape2d, SliceRange, TransformComponent}; @@ -152,7 +154,10 @@ impl<'a, R: Renderer> System<'a> for RenderSystem<'a, R> { } impl ComponentTemplate for RenderComponent { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { @@ -169,7 +174,7 @@ impl ComponentTemplate for RenderComponent { } }; - Ok(Box::new(Self { + Ok(Rc::new(Self { color: color.into(), shape, })) diff --git a/game/simulation/src/senses/system.rs b/game/simulation/src/senses/system.rs index 0c121a5c..1284ff5e 100644 --- a/game/simulation/src/senses/system.rs +++ b/game/simulation/src/senses/system.rs @@ -1,11 +1,13 @@ use crate::ecs::*; use crate::senses::sense::{HearingSphere, Sense, VisionCone}; use crate::spatial::Spatial; +use crate::string::StringCache; use crate::TransformComponent; use common::*; use serde::Deserialize; use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; +use std::rc::Rc; use unit::world::WorldPoint; /// Ticks to remember a sensed entity @@ -204,7 +206,10 @@ impl Debug for SensesComponent { } impl ComponentTemplate for MagicalSenseComponent { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { @@ -226,7 +231,7 @@ impl ComponentTemplate for MagicalSenseComponent { let vision: DeVision = values.get("vision").and_then(|v| v.into_type())?; // let hearing: DeHearing = values.get("hearing").and_then(|v| v.into_type())?; - Ok(Box::new(Self { + Ok(Rc::new(Self { vision: VisionCone { length: vision.length, angle: deg(vision.angle).into(), diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index dc079bba..ed3b8da7 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -122,9 +122,8 @@ impl Simulation { let voxel_world = world_loader.world(); // make ecs world and insert resources - let mut ecs_world = EcsWorld::new(); + let mut ecs_world = EcsWorld::with_definitions(definitions); ecs_world.insert(voxel_world.clone()); - ecs_world.insert(definitions); ecs_world.insert(string_cache); register_resources(&mut ecs_world, resources)?; diff --git a/game/simulation/src/society/job/job.rs b/game/simulation/src/society/job/job.rs index 15c9be70..26da1201 100644 --- a/game/simulation/src/society/job/job.rs +++ b/game/simulation/src/society/job/job.rs @@ -1,16 +1,18 @@ use crate::job::SocietyTask; -use crate::{Build, EcsWorld, Entity, StoneBrickWall, WorldPositionRange}; +use crate::{EcsWorld, Entity, WorldPositionRange}; use std::any::Any; use std::borrow::BorrowMut; use std::cell::RefCell; use common::*; +use crate::build::BuildTemplate; use crate::job::list::SocietyJobHandle; use crate::society::Society; + use std::ops::Deref; use std::rc::Rc; -use unit::world::{WorldPoint, WorldPosition}; +use unit::world::WorldPoint; /// A high-level society job that produces a number of [SocietyTask]s. Unsized but it lives in an /// Rc anyway @@ -65,8 +67,7 @@ pub trait SocietyJobImpl: Display + Debug { #[derive(Debug)] pub enum SocietyCommand { BreakBlocks(WorldPositionRange), - // TODO specify which build - Build(WorldPositionRange), + Build(WorldPositionRange, Rc), HaulToPosition(Entity, WorldPoint), /// (thing, container) @@ -89,10 +90,9 @@ impl SocietyCommand { match self { BreakBlocks(range) => job!(BreakBlocksJob::new(range)), - Build(pos) => { + Build(pos, template) => { for block in pos.iter_blocks() { - // TODO use actual job provided - jobs.submit(world, BuildThingJob::new(block, StoneBrickWall)) + jobs.submit(world, BuildThingJob::new(block, template.clone())) } } HaulToPosition(e, pos) => { diff --git a/game/simulation/src/society/job/jobs/build.rs b/game/simulation/src/society/job/jobs/build.rs index d7a1deda..db923c1b 100644 --- a/game/simulation/src/society/job/jobs/build.rs +++ b/game/simulation/src/society/job/jobs/build.rs @@ -1,5 +1,13 @@ +use std::collections::{HashMap, HashSet}; +use std::num::NonZeroU16; +use std::rc::Rc; + +use specs::BitSet; + +use common::*; + use crate::build::{ - Build, BuildMaterial, ConsumedMaterialForJobComponent, ReservedMaterialComponent, + BuildMaterial, BuildTemplate, ConsumedMaterialForJobComponent, ReservedMaterialComponent, }; use crate::definitions::{DefinitionNameComponent, DefinitionRegistry}; use crate::ecs::{EcsWorld, Join, WorldExt}; @@ -7,15 +15,10 @@ use crate::item::HaulableItemComponent; use crate::job::job::{CompletedTasks, SocietyJobImpl}; use crate::job::SocietyTaskResult; use crate::society::job::{SocietyJobHandle, SocietyTask}; +use crate::string::{CachedStr, CachedStringHasher}; use crate::{ BlockType, ComponentWorld, Entity, ItemStackComponent, TransformComponent, WorldPosition, }; -use common::*; -use specs::BitSet; - -use crate::string::{CachedStr, CachedStringHasher}; -use std::collections::{HashMap, HashSet}; -use std::num::NonZeroU16; // TODO build requirement engine for generic material combining // each job owns an instance, lends it to UI for rendering @@ -27,7 +30,7 @@ use std::num::NonZeroU16; pub struct BuildThingJob { // TODO support builds spanning multiple blocks/range position: WorldPosition, - build: Box, + build: Rc, required_materials: Vec, reserved_materials: HashSet, @@ -84,15 +87,14 @@ pub enum MaterialReservation { } impl BuildThingJob { - pub fn new(block: WorldPosition, build: impl Build + 'static) -> Self { - let mut materials = Vec::new(); - build.materials(&mut materials); - let count = materials.len(); + pub fn new(block: WorldPosition, build: Rc) -> Self { + let required_materials = build.materials().to_vec(); + let count = required_materials.len(); Self { position: block, - build: Box::new(build), + build, progress: 0, - required_materials: materials, + required_materials, hands_needed: HashMap::with_capacity_and_hasher(count, CachedStringHasher::default()), reserved_materials: HashSet::new(), materials_remaining: HashMap::with_hasher(CachedStringHasher::default()), // replaced on each call @@ -295,7 +297,10 @@ impl SocietyJobImpl for BuildThingJob { // preprocess materials to get hands needed for hauling let definitions = world.resource::(); for mat in self.required_materials.iter() { - let hands = match definitions.lookup_template(mat.definition(), "haulable") { + let def = definitions + .lookup_definition(mat.definition()) + .and_then(|def| def.find_component("haulable")); + let hands = match def { Some(any) => { let haulable = any .downcast_ref::() diff --git a/game/simulation/src/string.rs b/game/simulation/src/string.rs index 70d665e5..924f1df7 100644 --- a/game/simulation/src/string.rs +++ b/game/simulation/src/string.rs @@ -19,18 +19,14 @@ impl StringCache { CachedStr(ustr::ustr(s)) } - // TODO remove this - pub fn get_temporary(s: &str) -> CachedStr { - CachedStr(ustr::ustr(s)) - } - - #[cfg(test)] + /// Doesn't require a StringCache resource + #[cfg(any(test, feature = "testing"))] pub fn get_direct(s: &str) -> CachedStr { CachedStr(ustr::ustr(s)) } } -#[cfg(test)] +#[cfg(any(test, feature = "testing"))] impl From<&str> for CachedStr { fn from(s: &str) -> Self { StringCache::get_direct(s) diff --git a/game/simulation/src/transform.rs b/game/simulation/src/transform.rs index 66be5f85..7edce719 100644 --- a/game/simulation/src/transform.rs +++ b/game/simulation/src/transform.rs @@ -1,10 +1,12 @@ use common::*; +use std::rc::Rc; use unit::world::{WorldPoint, WorldPosition}; use crate::ecs::*; use crate::physics::{Bounds, PhysicsComponent}; +use crate::string::StringCache; use common::cgmath::Rotation; use serde::Deserialize; use unit::space::length::{Length, Length3}; @@ -159,13 +161,16 @@ pub struct PhysicalComponentTemplate { } impl ComponentTemplate for PhysicalComponentTemplate { - fn construct(values: &mut Map) -> Result>, ComponentBuildError> + fn construct( + values: &mut Map, + _: &StringCache, + ) -> Result>, ComponentBuildError> where Self: Sized, { let volume = values.get_int("volume")?; let size: Size = values.get("size")?.into_type()?; - Ok(Box::new(Self { + Ok(Rc::new(Self { volume: Volume::new(volume), size: Length3::new(size.x, size.y, size.z), })) diff --git a/game/world/src/block.rs b/game/world/src/block.rs index c0b9b11c..d14da942 100644 --- a/game/world/src/block.rs +++ b/game/world/src/block.rs @@ -26,7 +26,9 @@ pub type BlockDurability = u8; // TODO define block types in data instead of code /// The type of a block -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, EnumIter, Display)] +#[derive( + Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, EnumIter, EnumString, Display, +)] pub enum BlockType { Air, Dirt, diff --git a/renderer/engine/src/render/sdl/ui/windows/society.rs b/renderer/engine/src/render/sdl/ui/windows/society.rs index d5fb4968..77c52c21 100644 --- a/renderer/engine/src/render/sdl/ui/windows/society.rs +++ b/renderer/engine/src/render/sdl/ui/windows/society.rs @@ -1,14 +1,13 @@ use imgui::{im_str, ChildWindow, Selectable, StyleColor}; +use std::fmt::Display; use simulation::input::{SelectedEntity, SelectedTiles, UiRequest}; -use simulation::{ - AssociatedBlockData, Build, ComponentWorld, PlayerSociety, Societies, SocietyHandle, - StoneBrickWall, -}; +use simulation::{AssociatedBlockData, ComponentWorld, PlayerSociety, Societies, SocietyHandle}; use crate::render::sdl::ui::context::{DefaultOpen, UiContext}; use crate::render::sdl::ui::windows::{UiExt, COLOR_BLUE}; use crate::ui_str; + use serde::{Deserialize, Serialize}; use simulation::job::SocietyCommand; @@ -81,10 +80,14 @@ impl SocietyWindow { if context.button(im_str!("Build"), [0.0, 0.0]) { // TODO handle failure better? if let Some(above) = sel.range().above() { - context.issue_request(UiRequest::IssueSocietyCommand( - society_handle, - SocietyCommand::Build(above), - )); + if let Some((_, template, _)) = + ecs.build_templates().get(self.build_selection) + { + context.issue_request(UiRequest::IssueSocietyCommand( + society_handle, + SocietyCommand::Build(above, template.clone()), + )); + } } } @@ -95,10 +98,13 @@ impl SocietyWindow { .horizontal_scrollbar(true) .movable(false) .build(context.ui(), || { - // TODO temporary hardcoded list of builds - let builds = [&StoneBrickWall as &dyn Build]; - for (i, build) in builds.iter().enumerate() { - if Selectable::new(ui_str!(in context, "{}", build)) + let builds = ecs.build_templates(); + for (i, (id, _, name)) in builds.iter().enumerate() { + let name = match name { + Some(s) => s as &dyn Display, + None => id as &dyn Display, + }; + if Selectable::new(ui_str!(in context, "{}", name)) .selected(self.build_selection == i) .build(context) { diff --git a/renderer/main/src/scenarios.rs b/renderer/main/src/scenarios.rs index f678e053..3bf8c5a3 100644 --- a/renderer/main/src/scenarios.rs +++ b/renderer/main/src/scenarios.rs @@ -1,11 +1,10 @@ #![allow(dead_code)] use crate::scenarios::helpers::{spawn_entities_randomly, Placement}; -use crate::simulation::WorldPosition; use common::*; use engine::simulation; use simulation::job::BuildThingJob; -use simulation::{ComponentWorld, EcsWorld, PlayerSociety, Societies, StoneBrickWall}; +use simulation::{ComponentWorld, EcsWorld, PlayerSociety, Societies}; pub type Scenario = fn(&EcsWorld); const DEFAULT_SCENARIO: &str = "wander_and_eat"; @@ -172,12 +171,14 @@ fn building(ecs: &EcsWorld) { .society_by_handle(society) .expect("bad society"); - for _ in 0..8 { - let pos = helpers::random_walkable_pos(&world); + if let Some(build) = ecs.find_build_template("core_build_wall") { + for _ in 0..8 { + let pos = helpers::random_walkable_pos(&world); - society - .jobs_mut() - .submit(ecs, BuildThingJob::new(pos, StoneBrickWall)); + society + .jobs_mut() + .submit(ecs, BuildThingJob::new(pos, build.clone())); + } } } diff --git a/resources/definitions/builds/wall.ron b/resources/definitions/builds/wall.ron new file mode 100644 index 00000000..75322694 --- /dev/null +++ b/resources/definitions/builds/wall.ron @@ -0,0 +1,18 @@ +[ + ( + uid: "core_build_wall", + components: [ + {"build": ( + materials: [ + ("core_brick_stone", 6), + ], + steps: 10, + rate: 4, + output: "StoneBrickWall", + )}, + {"kind": ( + singular: "Stone brick wall", + )}, + ], + ), +] diff --git a/testing/src/tests/build.rs b/testing/src/tests/build.rs index ad3ac495..1924460e 100644 --- a/testing/src/tests/build.rs +++ b/testing/src/tests/build.rs @@ -1,19 +1,20 @@ -use crate::helpers::EntityPosition; -use crate::tests::TestHelper; -use crate::{HookContext, HookResult, InitHookResult, TestDeclaration}; +use std::cmp::Ordering; +use std::num::NonZeroU16; +use std::rc::Rc; + use common::*; use simulation::job::BuildThingJob; use simulation::{ - AiAction, BlockType, Build, BuildMaterial, ComponentWorld, ContainersError, Entity, - EntityEventDebugPayload, EntityEventPayload, HaulPurpose, HaulSource, ItemStackError, - QueuedUpdates, Societies, SocietyComponent, StackableComponent, TaskResultSummary, + BlockType, BuildMaterial, BuildTemplate, CachedStr, ComponentWorld, ContainersError, Entity, + EntityEventDebugPayload, EntityEventPayload, ItemStackError, Societies, SocietyComponent, + StackableComponent, TaskResultSummary, }; -use std::cell::RefCell; -use std::cmp::Ordering; -use std::num::NonZeroU16; -use std::rc::Rc; use unit::world::WorldPosition; +use crate::helpers::EntityPosition; +use crate::tests::TestHelper; +use crate::{HookContext, HookResult, InitHookResult, TestDeclaration}; + pub struct GatherAndBuild { builder: Entity, remaining_builds: usize, @@ -22,29 +23,9 @@ pub struct GatherAndBuild { all_bricks: Vec, } -#[derive(Debug)] -pub struct TestBrickWall; - const BRICKS_PER_WALL: u16 = 6; const MAX_STACKABILITY: u16 = 8; -impl Build for TestBrickWall { - fn output(&self) -> BlockType { - BlockType::StoneBrickWall - } - - fn progression(&self) -> (u32, u32) { - (4, 4) - } - - fn materials(&self, materials_out: &mut Vec) { - materials_out.push(BuildMaterial::new( - "core_brick_stone", - NonZeroU16::new(BRICKS_PER_WALL).unwrap(), - )) - } -} - impl GatherAndBuild { pub fn on_tick(&mut self, test: TestHelper, ctx: &HookContext) -> HookResult { let finished_builds = ctx @@ -171,10 +152,21 @@ impl GatherAndBuild { .copied() .map(|(x, y)| WorldPosition::from((x, y, 1))) .collect_vec(); + + let build = Rc::new(BuildTemplate::new( + vec![BuildMaterial::new( + CachedStr::from("core_brick_stone"), + NonZeroU16::new(BRICKS_PER_WALL).unwrap(), + )], + 4, + 4, + BlockType::StoneBrickWall, + )); + for pos in walls.iter() { society .jobs_mut() - .submit(ctx.simulation.ecs, BuildThingJob::new(*pos, TestBrickWall)); + .submit(ctx.simulation.ecs, BuildThingJob::new(*pos, build.clone())); } Ok(Self { From 1d92a44739c84f79837af16402911bbb47e73e40 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Wed, 19 Jan 2022 12:01:43 +0200 Subject: [PATCH 15/46] Use cached strings for component names too --- .planning/active.md | 2 +- TODOS.md | 4 +--- .../src/definitions/loader/step3_construction.rs | 9 ++++----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.planning/active.md b/.planning/active.md index c9c69fea..422fcbbe 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -16,6 +16,6 @@ * [o] register builds in data * [ ] validate build material definition exists? * [X] cache build templates intead of iterating all each time -* [ ] used cache strings for component names too +* [X] used cache strings for component names too * [ ] central registry of all builds including a unique identifier to refer to it in ui and its requests diff --git a/TODOS.md b/TODOS.md index 9568c664..a4c60292 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,4 +1,4 @@ -# TODOs (403) +# TODOs (402) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) @@ -198,8 +198,6 @@ * `// TODO remove abstract definitions` * [game/simulation/src/definitions/loader/mod.rs](game/simulation/src/definitions/loader/mod.rs) (1) * `// TODO consider using `nested` vecs as an optimization` - * [game/simulation/src/definitions/loader/step3_construction.rs](game/simulation/src/definitions/loader/step3_construction.rs) (1) - * `// TODO CachedStr for component names` * [game/simulation/src/definitions/mod.rs](game/simulation/src/definitions/mod.rs) (1) * `// TODO include which key caused the problem` * [game/simulation/src/ecs/component.rs](game/simulation/src/ecs/component.rs) (3) diff --git a/game/simulation/src/definitions/loader/step3_construction.rs b/game/simulation/src/definitions/loader/step3_construction.rs index b5f27ca8..e4aff730 100644 --- a/game/simulation/src/definitions/loader/step3_construction.rs +++ b/game/simulation/src/definitions/loader/step3_construction.rs @@ -14,8 +14,7 @@ use crate::string::{CachedStr, StringCache}; #[derive(Debug)] pub struct Definition { source: DefinitionSource, - // TODO CachedStr for component names - components: Vec<(String, Rc>)>, + components: Vec<(CachedStr, Rc>)>, } pub fn instantiate( @@ -52,7 +51,7 @@ impl Definition { pub fn find_component(&self, name: &str) -> Option<&dyn Any> { self.components.iter().find_map(|(comp, template)| { - if name == comp { + if name == comp.as_ref() { Some(template.as_any()) } else { None @@ -62,7 +61,7 @@ impl Definition { pub fn find_component_ref(&self, name: &str) -> Option> { self.components.iter().find_map(|(comp, template)| { - if name == comp && template.as_any().is::() { + if name == comp.as_ref() && template.as_any().is::() { let rc = template.clone(); // safety: type has been checked Some(unsafe { Rc::from_raw(Rc::into_raw(rc) as _) }) @@ -103,7 +102,7 @@ impl Definition { ); } - component_templates.push((key.as_str().to_owned(), component_template)); + component_templates.push((string_cache.get(&key), component_template)); } Ok((uid, component_templates)) From cf2836dade4b7bc2c63640c44e6678cafba7e7ef Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Wed, 19 Jan 2022 12:18:06 +0200 Subject: [PATCH 16/46] Add validation for build template materials --- .planning/active.md | 6 ++-- game/simulation/src/ecs/world.rs | 57 ++++++++++++++++++++++--------- game/simulation/src/simulation.rs | 2 +- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/.planning/active.md b/.planning/active.md index 422fcbbe..24623786 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -13,9 +13,9 @@ * [ ] can shrink/expand selection by 1 block * [ ] extend material reservation to include the specific materials in transit for a build, to avoid others considering hauling more when it's already on the way -* [o] register builds in data - * [ ] validate build material definition exists? +* [X] register builds in data + * [X] validate build material definition exists? * [X] cache build templates intead of iterating all each time * [X] used cache strings for component names too -* [ ] central registry of all builds including a unique identifier to refer to it in ui and its +* [X] central registry of all builds including a unique identifier to refer to it in ui and its requests diff --git a/game/simulation/src/ecs/world.rs b/game/simulation/src/ecs/world.rs index a9638860..5a847367 100644 --- a/game/simulation/src/ecs/world.rs +++ b/game/simulation/src/ecs/world.rs @@ -42,6 +42,10 @@ pub enum ComponentGetError { NoSuchComponent(Entity, &'static str), } +#[derive(Debug, Error)] +#[error("There are build templates with invalid materials: {0:?}")] +pub struct InvalidBuildTemplatesError(Vec); + /// Resource to hold entities to kill #[derive(Default)] pub struct EntitiesToKill { @@ -222,24 +226,45 @@ impl DerefMut for EcsWorld { } impl EcsWorld { - pub fn with_definitions(definitions: DefinitionRegistry) -> Self { + pub fn with_definitions( + definitions: DefinitionRegistry, + ) -> Result { let mut world = SpecsWorld::new(); let reg = ComponentRegistry::new(&mut world); - let build_templates = definitions - .iter_templates::("build") - .filter_map(|def| { - let definition = definitions.lookup_definition(def)?; - - let build = definition.find_component_ref::("build")?; - let name = definition - .find_component("kind") - .and_then(|any| any.downcast_ref::()) - .map(|kind| format!("{}", kind)); + // collect and validate build templates + let build_templates = { + let templates = definitions + .iter_templates::("build") + .filter_map(|def| { + let definition = definitions.lookup_definition(def)?; + + let build = definition.find_component_ref::("build")?; + + let name = definition + .find_component("kind") + .and_then(|any| any.downcast_ref::()) + .map(|kind| format!("{}", kind)); + + Some((def, build, name)) + }) + .collect_vec(); + + let mut invalids = Vec::new(); + for (def, build, _) in &templates { + for mat in build.materials() { + if definitions.lookup_definition(mat.definition()).is_none() { + invalids.push(format!("{}:{}", def, mat.definition())); + } + } + } - Some((def, build, name)) - }) - .collect_vec(); + if invalids.is_empty() { + templates + } else { + return Err(InvalidBuildTemplatesError(invalids)); + } + }; let mut world = EcsWorld { world, @@ -248,13 +273,13 @@ impl EcsWorld { }; world.world.insert(definitions); - world + Ok(world) } #[cfg(test)] pub fn new() -> Self { let reg = crate::definitions::load_from_str("[]").expect("can't load null definitions"); - Self::with_definitions(reg) + Self::with_definitions(reg).expect("invalid definitions") } /// Iterates through all known component types and checks each one diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index ed3b8da7..4f5efea7 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -122,7 +122,7 @@ impl Simulation { let voxel_world = world_loader.world(); // make ecs world and insert resources - let mut ecs_world = EcsWorld::with_definitions(definitions); + let mut ecs_world = EcsWorld::with_definitions(definitions)?; ecs_world.insert(voxel_world.clone()); ecs_world.insert(string_cache); register_resources(&mut ecs_world, resources)?; From dc3093e022b39e2d01eac586f2aa9f65a206cee5 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Wed, 19 Jan 2022 13:54:10 +0200 Subject: [PATCH 17/46] Improve job Display impl --- game/simulation/src/build/template.rs | 1 + game/simulation/src/society/job/jobs/build.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/game/simulation/src/build/template.rs b/game/simulation/src/build/template.rs index acb22619..630467d2 100644 --- a/game/simulation/src/build/template.rs +++ b/game/simulation/src/build/template.rs @@ -1,3 +1,4 @@ +use std::fmt::{Display, Formatter}; use std::num::NonZeroU16; use std::rc::Rc; diff --git a/game/simulation/src/society/job/jobs/build.rs b/game/simulation/src/society/job/jobs/build.rs index db923c1b..c76cc788 100644 --- a/game/simulation/src/society/job/jobs/build.rs +++ b/game/simulation/src/society/job/jobs/build.rs @@ -395,6 +395,6 @@ impl SocietyJobImpl for BuildThingJob { impl Display for BuildThingJob { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { // TODO better display impl for builds - write!(f, "Build {:?} at {}", self.build, self.position) + write!(f, "Build {} at {}", self.build.output(), self.position) } } From bec389373f356de7676ffbd737baaa0a1fbe2f63 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Thu, 20 Jan 2022 10:22:14 +0200 Subject: [PATCH 18/46] Add context menu popup experiment --- .planning/active.md | 2 + TODOS.md | 7 ++- game/simulation/src/input/mod.rs | 2 + game/simulation/src/input/popup.rs | 54 ++++++++++++++++++ game/simulation/src/input/system.rs | 23 +++++++- game/simulation/src/simulation.rs | 3 +- renderer/engine/src/render/sdl/backend.rs | 10 +++- renderer/engine/src/render/sdl/ui/render.rs | 61 ++++++++++++++++++--- 8 files changed, 145 insertions(+), 17 deletions(-) create mode 100644 game/simulation/src/input/popup.rs diff --git a/.planning/active.md b/.planning/active.md index 24623786..a9f97880 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -5,7 +5,9 @@ * [X] show ui block selection as it is dragged * [ ] block selection can be added to with ctrl+drag * [ ] show selection dimensions in world +* [o] test popups with an invisible imgui window * [ ] use left click for all selections, to allow right click context menu + * [ ] close popup on camera move * [X] ui for creating jobs for building many blocks * [ ] ui for wall outline specifically? * hovering over button should show outline preview diff --git a/TODOS.md b/TODOS.md index a4c60292..3922229f 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,4 +1,4 @@ -# TODOs (402) +# TODOs (404) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) @@ -228,9 +228,12 @@ * `// TODO might be better to just insert sorted` * [game/simulation/src/input/command.rs](game/simulation/src/input/command.rs) (1) * `// TODO expand/contract in a direction` - * [game/simulation/src/input/system.rs](game/simulation/src/input/system.rs) (3) + * [game/simulation/src/input/popup.rs](game/simulation/src/input/popup.rs) (1) + * `// TODO actual popup content` + * [game/simulation/src/input/system.rs](game/simulation/src/input/system.rs) (4) * `// TODO multiple clicks in the same place should iterate through all entities in selection range` * `// TODO spatial lookup for ui elements too` + * `// TODO temporary` * `// TODO select multiple entities` * [game/simulation/src/item/component.rs](game/simulation/src/item/component.rs) (7) * `// TODO proper nutritional value` diff --git a/game/simulation/src/input/mod.rs b/game/simulation/src/input/mod.rs index 2838cbf5..145bb78e 100644 --- a/game/simulation/src/input/mod.rs +++ b/game/simulation/src/input/mod.rs @@ -1,7 +1,9 @@ mod command; mod event; +mod popup; mod system; pub use command::*; pub use event::{InputEvent, MouseLocation, SelectType, SelectionProgress, WorldColumn}; +pub use popup::{PreparedUiPopup, UiPopup}; pub use system::{InputSystem, SelectedComponent, SelectedEntity, SelectedTiles}; diff --git a/game/simulation/src/input/popup.rs b/game/simulation/src/input/popup.rs new file mode 100644 index 00000000..7d575361 --- /dev/null +++ b/game/simulation/src/input/popup.rs @@ -0,0 +1,54 @@ +/// Single right click context menu +#[derive(Default)] +pub struct UiPopup { + popup: Option, + open: bool, +} + +pub struct PreparedUiPopup<'a>(&'a mut UiPopup); + +#[derive(Debug, Clone)] +pub enum PopupContent { + // TODO actual popup content + Test(String), +} + +impl UiPopup { + /// Opened at mouse position + pub fn open(&mut self, content: PopupContent) { + self.popup = Some(content); + self.open = true; + } + + fn on_close(&mut self) { + self.popup = None; + } + + /// Called once per frame by render system + pub fn prepare(&mut self) -> PreparedUiPopup { + PreparedUiPopup(self) + } +} + +impl PreparedUiPopup<'_> { + /// (popup, should open this frame) + pub fn iter_all(&self) -> impl Iterator + '_ { + self.0 + .popup + .as_ref() + .into_iter() + .map(move |content| (content, self.0.open)) + } + + pub fn on_close(&mut self) { + self.0.on_close() + } +} + +impl Drop for PreparedUiPopup<'_> { + fn drop(&mut self) { + if self.0.open { + self.0.open = false; + } + } +} diff --git a/game/simulation/src/input/system.rs b/game/simulation/src/input/system.rs index 3f3b8305..633bf7e4 100644 --- a/game/simulation/src/input/system.rs +++ b/game/simulation/src/input/system.rs @@ -2,6 +2,7 @@ use common::*; use unit::world::{WorldPosition, WorldPositionRange, WorldRange}; use crate::ecs::*; +use crate::input::popup::{PopupContent, UiPopup}; pub use crate::input::system::selected_tiles::SelectedTiles; use crate::input::{InputEvent, SelectType, SelectionProgress, WorldColumn}; use crate::spatial::{Spatial, Transforms}; @@ -32,6 +33,7 @@ impl<'a> System<'a> for InputSystem<'a> { Read<'a, Spatial>, Write<'a, SelectedEntity>, Write<'a, SelectedTiles>, + Write<'a, UiPopup>, WriteStorage<'a, SelectedComponent>, ReadStorage<'a, TransformComponent>, ReadStorage<'a, UiElementComponent>, @@ -39,7 +41,17 @@ impl<'a> System<'a> for InputSystem<'a> { fn run( &mut self, - (world, entities, spatial, mut selected, mut selected_block, mut selecteds, transform, ui): Self::SystemData, + ( + world, + entities, + spatial, + mut selected, + mut selected_block, + mut popups, + mut selecteds, + transform, + ui, + ): Self::SystemData, ) { let resolve_walkable_pos = |select_pos: &WorldColumn| { let world = (*world).borrow(); @@ -78,6 +90,9 @@ impl<'a> System<'a> for InputSystem<'a> { // find newly selected entity if let Some(to_select) = resolve_entity(pos) { selected.select_with_comps(&mut selecteds, to_select); + + // TODO temporary + popups.open(PopupContent::Test(format!("wahey {}", to_select))); } } @@ -381,10 +396,12 @@ mod selected_tiles { #[cfg(test)] mod tests { - use super::*; - use crate::input::system::selected_tiles::BlockOccurrences; use world::block::BlockType; + use crate::input::system::selected_tiles::BlockOccurrences; + + use super::*; + #[test] fn comma_separated_blocks() { let mut map = BTreeMap::new(); diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index 4f5efea7..22e43e95 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -15,7 +15,7 @@ use crate::ecs::*; use crate::event::{DeathReason, EntityEventQueue, RuntimeTimers}; use crate::input::{ BlockPlacement, DivineInputCommand, InputEvent, InputSystem, MouseLocation, SelectedEntity, - SelectedTiles, UiCommand, UiRequest, UiResponsePayload, + SelectedTiles, UiCommand, UiPopup, UiRequest, UiResponsePayload, WorldColumn, }; use crate::item::{ContainerComponent, HaulSystem}; use crate::movement::MovementFulfilmentSystem; @@ -682,6 +682,7 @@ fn register_resources(world: &mut EcsWorld, resources: Resources) -> BoxedResult world.insert(MouseLocation::default()); world.insert(NameGeneration::load(&resources)?); world.insert(FrameAllocator::default()); + world.insert(UiPopup::default()); Ok(()) } diff --git a/renderer/engine/src/render/sdl/backend.rs b/renderer/engine/src/render/sdl/backend.rs index 0db27b04..cf156599 100644 --- a/renderer/engine/src/render/sdl/backend.rs +++ b/renderer/engine/src/render/sdl/backend.rs @@ -9,8 +9,8 @@ use color::Color; use common::input::{CameraDirection, GameKey, KeyAction, RendererKey}; use common::*; use simulation::{ - BackendData, Exit, InitializedSimulationBackend, PerfAvg, PersistentSimulationBackend, - Simulation, WorldViewer, + BackendData, ComponentWorld, Exit, InitializedSimulationBackend, PerfAvg, + PersistentSimulationBackend, Simulation, WorldViewer, }; use crate::render::sdl::camera::Camera; @@ -22,7 +22,9 @@ use crate::render::sdl::GlRenderer; use resources::ResourceError; use resources::Resources; use sdl2::mouse::{MouseButton, MouseState, MouseWheelDirection}; -use simulation::input::{InputEvent, SelectType, UiCommand, UiCommands, UiRequest, WorldColumn}; +use simulation::input::{ + InputEvent, SelectType, UiCommand, UiCommands, UiPopup, UiRequest, WorldColumn, +}; use std::hint::unreachable_unchecked; use unit::world::{WorldPoint, WorldPoint2d, WorldPosition}; @@ -343,12 +345,14 @@ impl InitializedSimulationBackend for SdlBackendInit { // render ui and collect input commands let mouse_state = self.backend.mouse_state(); + let popups = simulation.world().resource_mut::().prepare(); self.backend.ui.render( &self.backend.window, &mouse_state, perf, simulation.as_ref(&self.world_viewer), commands, + popups, ); self.window.gl_swap_window(); diff --git a/renderer/engine/src/render/sdl/ui/render.rs b/renderer/engine/src/render/sdl/ui/render.rs index b77a0c5f..54fed241 100644 --- a/renderer/engine/src/render/sdl/ui/render.rs +++ b/renderer/engine/src/render/sdl/ui/render.rs @@ -1,4 +1,7 @@ -use imgui::{im_str, Condition, Context, FontConfig, FontSource, Style}; +use std::io::{ErrorKind, Read, Write}; +use std::path::Path; + +use imgui::{im_str, Condition, Context, FontConfig, FontSource, Style, StyleVar, WindowFlags}; use imgui_opengl_renderer::Renderer; use imgui_sdl2::ImguiSdl2; use sdl2::event::Event; @@ -7,7 +10,8 @@ use sdl2::video::Window; use sdl2::VideoSubsystem; use serde::{Deserialize, Serialize}; -use simulation::input::{UiCommand, UiCommands, UiRequest}; +use common::BoxedResult; +use simulation::input::{PreparedUiPopup, UiCommand, UiCommands, UiRequest}; use simulation::{PerfAvg, SimulationRef}; use crate::render::sdl::ui::context::UiContext; @@ -15,10 +19,7 @@ use crate::render::sdl::ui::memory::PerFrameStrings; use crate::render::sdl::ui::windows::{ DebugWindow, PerformanceWindow, SelectionWindow, SocietyWindow, }; -use common::BoxedResult; - -use std::io::{ErrorKind, Read, Write}; -use std::path::Path; +use crate::ui_str; pub struct Ui { imgui: Context, @@ -116,6 +117,7 @@ impl Ui { perf: PerfAvg, simulation: SimulationRef, commands: &mut UiCommands, + popup: PreparedUiPopup, ) { self.imgui_sdl2 .prepare_frame(self.imgui.io_mut(), window, mouse_state); @@ -123,7 +125,7 @@ impl Ui { // generate windows let context = UiContext::new(&ui, &self.strings_arena, simulation, commands, perf); - self.state.render(context); + self.state.render(context, popup); // render windows self.imgui_sdl2.prepare_render(&ui, window); @@ -202,7 +204,7 @@ impl Ui { impl State { /// Renders ui windows - fn render(&mut self, context: UiContext) { + fn render(&mut self, context: UiContext, mut popup: PreparedUiPopup) { imgui::Window::new(im_str!("Debug")) .size([400.0, 500.0], Condition::FirstUseEver) .position([10.0, 10.0], Condition::FirstUseEver) @@ -222,5 +224,48 @@ impl State { self.society.render(&context); self.debug.render(&context); }); + + // invisible window for popups + let style = context.push_style_var(StyleVar::WindowRounding(0.0)); + imgui::Window::new(im_str!("invisible")) + .size(context.io().display_size, Condition::Always) + .position([0.0, 0.0], Condition::Always) + .bg_alpha(0.0) + .flags( + WindowFlags::NO_TITLE_BAR + | WindowFlags::NO_INPUTS + | WindowFlags::NO_RESIZE + | WindowFlags::NO_COLLAPSE + | WindowFlags::NO_BACKGROUND + | WindowFlags::NO_SAVED_SETTINGS + | WindowFlags::NO_BRING_TO_FRONT_ON_FOCUS, + ) + .build(context.ui(), || { + enum Rendered { + No, + OpenAndRendered, + OpenButNotRendered, + } + + let mut rendered = Rendered::No; + for (content, open) in popup.iter_all() { + let id = im_str!("popup"); + rendered = Rendered::OpenButNotRendered; + + if open { + context.open_popup(id); + } + + context.popup(id, || { + context.text(ui_str!(in context, "{:?}", content)); + rendered = Rendered::OpenAndRendered; + }); + } + + if let Rendered::OpenButNotRendered = rendered { + popup.on_close(); + } + }); + style.pop(context.ui()); } } From f630ef75bde24b12eea3c5be9fa429ec3b4e96c6 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Thu, 20 Jan 2022 11:08:04 +0200 Subject: [PATCH 19/46] Add skeleton right click context menu --- .planning/active.md | 7 +- TODOS.md | 9 +- game/simulation/src/input/popup.rs | 6 +- game/simulation/src/input/system.rs | 120 +++++++++++++------- game/simulation/src/render/renderer.rs | 7 +- game/simulation/src/render/system.rs | 8 +- game/simulation/src/simulation.rs | 2 +- renderer/engine/src/render/sdl/ui/render.rs | 1 + 8 files changed, 101 insertions(+), 59 deletions(-) diff --git a/.planning/active.md b/.planning/active.md index a9f97880..c4e8b624 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -5,9 +5,11 @@ * [X] show ui block selection as it is dragged * [ ] block selection can be added to with ctrl+drag * [ ] show selection dimensions in world -* [o] test popups with an invisible imgui window - * [ ] use left click for all selections, to allow right click context menu +* popups and ui selection changes + * [X] test popups with an invisible imgui window + * [o] selection-sensitive right click context menu * [ ] close popup on camera move + * [ ] escape for clearing tile and entity selection (rebind quit) * [X] ui for creating jobs for building many blocks * [ ] ui for wall outline specifically? * hovering over button should show outline preview @@ -21,3 +23,4 @@ * [X] used cache strings for component names too * [X] central registry of all builds including a unique identifier to refer to it in ui and its requests +* [ ] update controls in readme diff --git a/TODOS.md b/TODOS.md index 3922229f..70d7b88c 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,4 +1,4 @@ -# TODOs (404) +# TODOs (403) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) @@ -228,12 +228,9 @@ * `// TODO might be better to just insert sorted` * [game/simulation/src/input/command.rs](game/simulation/src/input/command.rs) (1) * `// TODO expand/contract in a direction` - * [game/simulation/src/input/popup.rs](game/simulation/src/input/popup.rs) (1) - * `// TODO actual popup content` - * [game/simulation/src/input/system.rs](game/simulation/src/input/system.rs) (4) + * [game/simulation/src/input/system.rs](game/simulation/src/input/system.rs) (3) * `// TODO multiple clicks in the same place should iterate through all entities in selection range` * `// TODO spatial lookup for ui elements too` - * `// TODO temporary` * `// TODO select multiple entities` * [game/simulation/src/item/component.rs](game/simulation/src/item/component.rs) (7) * `// TODO proper nutritional value` @@ -494,6 +491,8 @@ * `// TODO customise text colour` * `// TODO use instances or indices?` * `let _no_depth = Capability::DepthTest.scoped_disable(); // TODO clear depth mask instead` + * [renderer/engine/src/render/sdl/ui/render.rs](renderer/engine/src/render/sdl/ui/render.rs) (1) + * `// TODO actual popup content` * [renderer/engine/src/render/sdl/ui/windows/debug_renderer.rs](renderer/engine/src/render/sdl/ui/windows/debug_renderer.rs) (1) * `// TODO proper default script path` * [renderer/engine/src/render/sdl/ui/windows/selection.rs](renderer/engine/src/render/sdl/ui/windows/selection.rs) (9) diff --git a/game/simulation/src/input/popup.rs b/game/simulation/src/input/popup.rs index 7d575361..6a99cd1d 100644 --- a/game/simulation/src/input/popup.rs +++ b/game/simulation/src/input/popup.rs @@ -1,3 +1,5 @@ +use crate::Entity; + /// Single right click context menu #[derive(Default)] pub struct UiPopup { @@ -9,8 +11,8 @@ pub struct PreparedUiPopup<'a>(&'a mut UiPopup); #[derive(Debug, Clone)] pub enum PopupContent { - // TODO actual popup content - Test(String), + TileSelection, + Entity(Entity), } impl UiPopup { diff --git a/game/simulation/src/input/system.rs b/game/simulation/src/input/system.rs index 633bf7e4..3b82c9c1 100644 --- a/game/simulation/src/input/system.rs +++ b/game/simulation/src/input/system.rs @@ -1,5 +1,5 @@ use common::*; -use unit::world::{WorldPosition, WorldPositionRange, WorldRange}; +use unit::world::{WorldPoint, WorldPosition, WorldPositionRange, WorldRange}; use crate::ecs::*; use crate::input::popup::{PopupContent, UiPopup}; @@ -10,7 +10,7 @@ use crate::TransformComponent; use crate::{UiElementComponent, WorldRef}; pub struct InputSystem<'a> { - pub events: &'a [InputEvent], + events: &'a [InputEvent], } /// Marker for entity selection by the player @@ -26,6 +26,22 @@ pub struct SelectedComponent; pub struct SelectedEntity(Option); const TILE_SELECTION_LIMIT: f32 = 50.0; +impl<'a> InputSystem<'a> { + /// Events for this tick + pub fn with_events(events: &'a [InputEvent]) -> Self { + Self { events } + } + + fn resolve_walkable_pos( + &self, + select_pos: &WorldColumn, + world: &WorldRef, + ) -> Option { + let world = (*world).borrow(); + select_pos.find_highest_walkable(&world) + } +} + impl<'a> System<'a> for InputSystem<'a> { type SystemData = ( Read<'a, WorldRef>, @@ -53,49 +69,31 @@ impl<'a> System<'a> for InputSystem<'a> { ui, ): Self::SystemData, ) { - let resolve_walkable_pos = |select_pos: &WorldColumn| { - let world = (*world).borrow(); - select_pos.find_highest_walkable(&world) - }; - let resolve_entity = |select_pos: &WorldColumn| { - resolve_walkable_pos(select_pos).and_then(|point| { - // TODO multiple clicks in the same place should iterate through all entities in selection range - - const RADIUS: f32 = 1.25; - - // prioritise ui elements first - // TODO spatial lookup for ui elements too - let ui_elem = (&entities, &transform, &ui) - .join() - .find(|(_, transform, _)| transform.position.is_almost(&point, RADIUS)) - .map(|(e, _, _)| e.into()); - - // fallack to looking for normal entities - ui_elem.or_else(|| { - spatial - .query_in_radius(Transforms::Storage(&transform), point, RADIUS) - .next() - .map(|(e, _, _)| e) + self.resolve_walkable_pos(select_pos, &world) + .and_then(|point| { + // TODO multiple clicks in the same place should iterate through all entities in selection range + const RADIUS: f32 = 1.25; + + // prioritise ui elements first + // TODO spatial lookup for ui elements too + let ui_elem = (&entities, &transform, &ui) + .join() + .find(|(_, transform, _)| transform.position.is_almost(&point, RADIUS)) + .map(|(e, _, _)| e.into()); + + // fallback to looking for normal entities + ui_elem.or_else(|| { + spatial + .query_in_radius(Transforms::Storage(&transform), point, RADIUS) + .next() + .map(|(e, _, _)| e) + }) }) - }) }; for e in self.events { match e { - InputEvent::Click(SelectType::Left, pos) => { - // unselect current entity regardless of click location - selected.unselect_with_comps(&mut selecteds); - - // find newly selected entity - if let Some(to_select) = resolve_entity(pos) { - selected.select_with_comps(&mut selecteds, to_select); - - // TODO temporary - popups.open(PopupContent::Test(format!("wahey {}", to_select))); - } - } - InputEvent::Select { select: SelectType::Left, .. @@ -103,11 +101,6 @@ impl<'a> System<'a> for InputSystem<'a> { // TODO select multiple entities } - InputEvent::Click(SelectType::Right, _) => { - // unselect tile selection - selected_block.clear(); - } - InputEvent::Select { select: SelectType::Right, from, @@ -117,6 +110,26 @@ impl<'a> System<'a> for InputSystem<'a> { // update tile selection selected_block.update((*from, *to), *progress, &world); } + + InputEvent::Click(SelectType::Left, pos) => { + // unselect current entity regardless of click location + selected.unselect_with_comps(&mut selecteds); + + // find newly selected entity + if let Some(to_select) = resolve_entity(pos) { + selected.select_with_comps(&mut selecteds, to_select); + } + } + + InputEvent::Click(SelectType::Right, pos) => { + if selected_block.is_right_click_relevant(pos) { + // show popup for selection + popups.open(PopupContent::TileSelection); + } else if let Some(entity) = resolve_entity(pos) { + // show popup for entity + popups.open(PopupContent::Entity(entity)); + } + } } } } @@ -291,6 +304,25 @@ mod selected_tiles { sel.on_world_change(world.borrow()) } } + + /// Is the given right click position close enough to the bottom right of the active + /// selection + pub fn is_right_click_relevant(&self, pos: &WorldColumn) -> bool { + const DISTANCE_THRESHOLD: f32 = 2.0; + self.current_selected() + .map(|sel| { + // distance check to bottom right corner, ignoring z axis + + let corner = { + let ((_, x2), (y1, _), _) = sel.range.ranges(); + Point2::new(x2 as f32, y1 as f32) + }; + let click = Point2::new(pos.x.into_inner(), pos.y.into_inner()); + + corner.distance2(click) <= DISTANCE_THRESHOLD * DISTANCE_THRESHOLD + }) + .unwrap_or(false) + } } impl CurrentSelection { diff --git a/game/simulation/src/render/renderer.rs b/game/simulation/src/render/renderer.rs index bdd5f853..18826250 100644 --- a/game/simulation/src/render/renderer.rs +++ b/game/simulation/src/render/renderer.rs @@ -65,7 +65,7 @@ pub trait Renderer { // ---- - fn tile_selection(&mut self, a: WorldPosition, b: WorldPosition, color: Color) { + fn tile_selection(&mut self, a: WorldPosition, b: WorldPosition, color: Color, finished: bool) { let (ax, ay, az) = WorldPoint::from(a).xyz(); let (bx, by, bz) = WorldPoint::from(b).xyz(); @@ -90,6 +90,11 @@ pub trait Renderer { self.debug_add_quad([bl, br, tr, tl], color); // TODO render translucent quad over selected blocks, showing which are visible/occluded. cache this mesh + + // thing to right click on in bottom right corner + if finished { + self.debug_add_square_around(br, 0.15, color); + } } fn debug_add_square_around(&mut self, centre: WorldPoint, radius: f32, color: Color) { diff --git a/game/simulation/src/render/system.rs b/game/simulation/src/render/system.rs index fc0ea424..f38b3eba 100644 --- a/game/simulation/src/render/system.rs +++ b/game/simulation/src/render/system.rs @@ -109,11 +109,11 @@ impl<'a, R: Renderer> System<'a> for RenderSystem<'a, R> { // render player's world selection if let Some(sel) = selected_block.current() { let (from, to) = sel.bounds(); - let color = match sel.progress() { - SelectionProgress::InProgress => Color::rgb(216, 221, 230), - SelectionProgress::Complete => Color::rgb(252, 253, 255), + let (color, finished) = match sel.progress() { + SelectionProgress::InProgress => (Color::rgb(216, 221, 230), false), + SelectionProgress::Complete => (Color::rgb(252, 253, 255), true), }; - self.renderer.tile_selection(from, to, color); + self.renderer.tile_selection(from, to, color, finished); } // render in-game ui elements above entities diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index 22e43e95..b778c514 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -507,7 +507,7 @@ impl Simulation { input: &[InputEvent], ) -> R::FrameContext { // process input before rendering - InputSystem { events: input }.run_now(&*self.ecs_world); + InputSystem::with_events(input).run_now(&*self.ecs_world); // start frame renderer.init(target); diff --git a/renderer/engine/src/render/sdl/ui/render.rs b/renderer/engine/src/render/sdl/ui/render.rs index 54fed241..14fc1720 100644 --- a/renderer/engine/src/render/sdl/ui/render.rs +++ b/renderer/engine/src/render/sdl/ui/render.rs @@ -257,6 +257,7 @@ impl State { } context.popup(id, || { + // TODO actual popup content context.text(ui_str!(in context, "{:?}", content)); rendered = Rendered::OpenAndRendered; }); From d2db016bd8bb7705a151e2f09ed525d92f4ca8a5 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Thu, 20 Jan 2022 11:19:52 +0200 Subject: [PATCH 20/46] Remap escape to clear active popup/selection --- .planning/active.md | 2 +- game/simulation/src/input/command.rs | 2 + game/simulation/src/input/popup.rs | 11 ++++++ game/simulation/src/simulation.rs | 27 +++++++++---- renderer/engine/src/render/sdl/backend.rs | 48 +++++++++++++++-------- shared/common/src/input.rs | 9 ++++- 6 files changed, 71 insertions(+), 28 deletions(-) diff --git a/.planning/active.md b/.planning/active.md index c4e8b624..ba6db221 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -9,7 +9,7 @@ * [X] test popups with an invisible imgui window * [o] selection-sensitive right click context menu * [ ] close popup on camera move - * [ ] escape for clearing tile and entity selection (rebind quit) + * [X] escape for clearing tile and entity selection (rebind quit) * [X] ui for creating jobs for building many blocks * [ ] ui for wall outline specifically? * hovering over button should show outline preview diff --git a/game/simulation/src/input/command.rs b/game/simulation/src/input/command.rs index bfa35b0e..de22b17a 100644 --- a/game/simulation/src/input/command.rs +++ b/game/simulation/src/input/command.rs @@ -43,6 +43,8 @@ pub enum UiRequest { }, ModifySelection(SelectionModification), + + CancelSelection, } pub enum SelectionModification { diff --git a/game/simulation/src/input/popup.rs b/game/simulation/src/input/popup.rs index 6a99cd1d..6ec4ba38 100644 --- a/game/simulation/src/input/popup.rs +++ b/game/simulation/src/input/popup.rs @@ -26,6 +26,17 @@ impl UiPopup { self.popup = None; } + /// Returns true if closed + pub fn close(&mut self) -> bool { + if self.popup.is_some() { + self.popup = None; + self.open = false; + true + } else { + false + } + } + /// Called once per frame by render system pub fn prepare(&mut self) -> PreparedUiPopup { PreparedUiPopup(self) diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index b778c514..3d018f48 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -1,8 +1,11 @@ +use std::collections::HashSet; use std::ops::{Add, Deref}; +use std::pin::Pin; + +use strum::EnumDiscriminants; use common::*; use resources::Resources; -use strum::EnumDiscriminants; use unit::world::{WorldPosition, WorldPositionRange}; use world::block::BlockType; use world::loader::{TerrainUpdatesRes, WorldTerrainUpdate}; @@ -10,7 +13,7 @@ use world::WorldChangeEvent; use crate::activity::ActivitySystem; use crate::ai::{AiAction, AiComponent, AiSystem}; - +use crate::alloc::FrameAllocator; use crate::ecs::*; use crate::event::{DeathReason, EntityEventQueue, RuntimeTimers}; use crate::input::{ @@ -28,12 +31,9 @@ use crate::render::{ DebugRenderersState, UiElementPruneSystem, }; use crate::render::{RenderSystem, Renderer}; -use crate::senses::{SensesDebugRenderer, SensesSystem}; - -use crate::alloc::FrameAllocator; - use crate::runtime::{Runtime, RuntimeSystem}; use crate::scripting::ScriptingContext; +use crate::senses::{SensesDebugRenderer, SensesSystem}; use crate::society::{NameGeneration, PlayerSociety}; use crate::spatial::{Spatial, SpatialSystem}; use crate::steer::{SteeringDebugRenderer, SteeringSystem}; @@ -44,8 +44,6 @@ use crate::{ ThreadedWorldLoader, WorldRef, WorldViewer, }; use crate::{ComponentWorld, Societies, SocietyHandle}; -use std::collections::HashSet; -use std::pin::Pin; #[derive(Debug, EnumDiscriminants)] #[strum_discriminants(name(AssociatedBlockDataType))] @@ -491,6 +489,19 @@ impl Simulation { let sel = self.ecs_world.resource_mut::(); sel.modify(modification, &self.voxel_world); } + + UiRequest::CancelSelection => { + // close current popup if there is one + let popup = self.ecs_world.resource_mut::(); + if !popup.close() { + // fallback to clearing tile and entity selections + let tiles = self.ecs_world.resource_mut::(); + let entity = self.ecs_world.resource_mut::(); + + tiles.clear(); + entity.unselect(&self.ecs_world); + } + } } } diff --git a/renderer/engine/src/render/sdl/backend.rs b/renderer/engine/src/render/sdl/backend.rs index cf156599..feb55208 100644 --- a/renderer/engine/src/render/sdl/backend.rs +++ b/renderer/engine/src/render/sdl/backend.rs @@ -1,17 +1,25 @@ +use std::hint::unreachable_unchecked; use std::ops::{Deref, DerefMut}; use sdl2::event::{Event, WindowEvent}; use sdl2::keyboard::{Keycode, Mod}; +use sdl2::mouse::{MouseButton, MouseState, MouseWheelDirection}; use sdl2::video::{Window, WindowBuildError}; use sdl2::{EventPump, Sdl, VideoSubsystem}; use color::Color; -use common::input::{CameraDirection, GameKey, KeyAction, RendererKey}; +use common::input::{CameraDirection, EngineKey, GameKey, KeyAction, RendererKey}; use common::*; +use resources::ResourceError; +use resources::Resources; +use simulation::input::{ + InputEvent, SelectType, UiCommand, UiCommands, UiPopup, UiRequest, WorldColumn, +}; use simulation::{ BackendData, ComponentWorld, Exit, InitializedSimulationBackend, PerfAvg, PersistentSimulationBackend, Simulation, WorldViewer, }; +use unit::world::{WorldPoint, WorldPoint2d, WorldPosition}; use crate::render::sdl::camera::Camera; use crate::render::sdl::gl::{Gl, GlError}; @@ -19,14 +27,6 @@ use crate::render::sdl::render::GlFrameContext; use crate::render::sdl::selection::Selection; use crate::render::sdl::ui::{EventConsumed, Ui}; use crate::render::sdl::GlRenderer; -use resources::ResourceError; -use resources::Resources; -use sdl2::mouse::{MouseButton, MouseState, MouseWheelDirection}; -use simulation::input::{ - InputEvent, SelectType, UiCommand, UiCommands, UiPopup, UiRequest, WorldColumn, -}; -use std::hint::unreachable_unchecked; -use unit::world::{WorldPoint, WorldPoint2d, WorldPosition}; pub struct SdlBackendPersistent { camera: Camera, @@ -194,7 +194,7 @@ impl InitializedSimulationBackend for SdlBackendInit { keycode: Some(key), keymod, .. - } => match map_sdl_keycode(key) { + } => match map_sdl_keycode(key, keymod) { Some(action) => { let ui_req = self.handle_key(action, keymod, true); commands.extend(ui_req.map(UiCommand::new).into_iter()); @@ -207,7 +207,7 @@ impl InitializedSimulationBackend for SdlBackendInit { keymod, .. } => { - if let Some(action) = map_sdl_keycode(key) { + if let Some(action) = map_sdl_keycode(key, keymod) { let ui_req = self.handle_key(action, keymod, false); commands.extend(ui_req.map(UiCommand::new).into_iter()); } @@ -429,11 +429,11 @@ impl SdlBackendInit { // no ui command to return None } - KeyAction::Game(key) => { + KeyAction::Engine(key) => { if is_down { let cmd = match key { - GameKey::Exit => UiRequest::ExitGame(Exit::Stop), - GameKey::Restart => UiRequest::ExitGame(Exit::Restart), + EngineKey::Exit => UiRequest::ExitGame(Exit::Stop), + EngineKey::Restart => UiRequest::ExitGame(Exit::Restart), }; Some(cmd) @@ -442,6 +442,17 @@ impl SdlBackendInit { None } } + + KeyAction::Game(key) => { + // send input to game on down only + if is_down { + Some(match key { + GameKey::CancelSelection => UiRequest::CancelSelection, + }) + } else { + None + } + } } } @@ -484,13 +495,15 @@ impl SdlBackendPersistent { // - impl std::convert::TryInto for T // where U: std::convert::TryFrom; // = note: upstream crates may add a new impl of trait `std::convert::From` for type `common::input::Key` in future versions -fn map_sdl_keycode(keycode: Keycode) -> Option { +fn map_sdl_keycode(keycode: Keycode, keymod: Mod) -> Option { use Keycode::*; use RendererKey::*; + let alt_down = || keymod.intersects(Mod::LALTMOD | Mod::RALTMOD); + Some(match keycode { - Escape => KeyAction::Game(GameKey::Exit), - R => KeyAction::Game(GameKey::Restart), + Escape if alt_down() => KeyAction::Engine(EngineKey::Exit), + R if alt_down() => KeyAction::Engine(EngineKey::Restart), Up => KeyAction::Renderer(SliceUp), Down => KeyAction::Renderer(SliceDown), @@ -500,6 +513,7 @@ fn map_sdl_keycode(keycode: Keycode) -> Option { S => KeyAction::Renderer(Camera(CameraDirection::Down)), D => KeyAction::Renderer(Camera(CameraDirection::Right)), + Escape => KeyAction::Game(GameKey::CancelSelection), _ => return None, }) } diff --git a/shared/common/src/input.rs b/shared/common/src/input.rs index b36ad367..8f998d7a 100644 --- a/shared/common/src/input.rs +++ b/shared/common/src/input.rs @@ -20,14 +20,19 @@ pub enum RendererKey { } #[derive(Copy, Clone, Debug)] -pub enum GameKey { +pub enum EngineKey { Exit, Restart, } +#[derive(Copy, Clone, Debug)] +pub enum GameKey { + CancelSelection, +} + pub enum KeyAction { - /// Renderer action e.g. move camera Renderer(RendererKey), + Engine(EngineKey), Game(GameKey), } From ff155b4f2d3d62d778a3720ff7020cbf3711103f Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Thu, 20 Jan 2022 11:23:07 +0200 Subject: [PATCH 21/46] Close popup on camera movement --- .planning/active.md | 2 +- game/simulation/src/input/command.rs | 4 ++++ game/simulation/src/simulation.rs | 4 ++++ renderer/engine/src/render/sdl/backend.rs | 9 +++++---- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.planning/active.md b/.planning/active.md index ba6db221..6736f82c 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -8,7 +8,7 @@ * popups and ui selection changes * [X] test popups with an invisible imgui window * [o] selection-sensitive right click context menu - * [ ] close popup on camera move + * [X] close popup on camera move * [X] escape for clearing tile and entity selection (rebind quit) * [X] ui for creating jobs for building many blocks * [ ] ui for wall outline specifically? diff --git a/game/simulation/src/input/command.rs b/game/simulation/src/input/command.rs index de22b17a..9b8fb651 100644 --- a/game/simulation/src/input/command.rs +++ b/game/simulation/src/input/command.rs @@ -44,6 +44,10 @@ pub enum UiRequest { ModifySelection(SelectionModification), + /// Closes current popup if any + CancelPopup, + + /// Closes current popup if any then clears entity+tile selection CancelSelection, } diff --git a/game/simulation/src/simulation.rs b/game/simulation/src/simulation.rs index 3d018f48..ff9ac499 100644 --- a/game/simulation/src/simulation.rs +++ b/game/simulation/src/simulation.rs @@ -502,6 +502,10 @@ impl Simulation { entity.unselect(&self.ecs_world); } } + UiRequest::CancelPopup => { + // close current popup only + self.ecs_world.resource_mut::().close(); + } } } diff --git a/renderer/engine/src/render/sdl/backend.rs b/renderer/engine/src/render/sdl/backend.rs index feb55208..9158707c 100644 --- a/renderer/engine/src/render/sdl/backend.rs +++ b/renderer/engine/src/render/sdl/backend.rs @@ -414,20 +414,21 @@ impl SdlBackendInit { // move by 1 slice self.world_viewer.move_by(delta); } + + None } (_, Camera(direction)) => { self.camera.handle_move(direction, is_down); + Some(UiRequest::CancelPopup) } _ => { if is_down { warn!("unhandled key down: {:?}", key); } + None } - }; - - // no ui command to return - None + } } KeyAction::Engine(key) => { if is_down { From 8842c48ceb18a0b8886936c36441f214d0d92004 Mon Sep 17 00:00:00 2001 From: Dom Williams Date: Thu, 20 Jan 2022 12:36:35 +0200 Subject: [PATCH 22/46] Add skeleton for context menu contents --- .planning/active.md | 4 +- TODOS.md | 9 +- game/simulation/src/ecs/world.rs | 6 + game/simulation/src/input/popup.rs | 134 +++++++++++++++++--- game/simulation/src/input/system.rs | 6 +- renderer/engine/src/render/sdl/ui/render.rs | 17 ++- 6 files changed, 145 insertions(+), 31 deletions(-) diff --git a/.planning/active.md b/.planning/active.md index 6736f82c..bf2d5cf9 100644 --- a/.planning/active.md +++ b/.planning/active.md @@ -7,7 +7,9 @@ * [ ] show selection dimensions in world * popups and ui selection changes * [X] test popups with an invisible imgui window - * [o] selection-sensitive right click context menu + * [X] selection-sensitive right click context menu + * [ ] populate for entity selection + * [ ] populate for tile selection * [X] close popup on camera move * [X] escape for clearing tile and entity selection (rebind quit) * [X] ui for creating jobs for building many blocks diff --git a/TODOS.md b/TODOS.md index 70d7b88c..ca54b567 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,8 +1,10 @@ -# TODOs (403) +# TODOs (406) * [.build/build-release.sh](.build/build-release.sh) (1) * `# TODO declare sdl version somewhere else` * [.build/run-tests.sh](.build/run-tests.sh) (1) * `# TODO fix "LNK1189: library limit of 65535 objects exceeded" on windows when building `testing` crate` + * [.planning/active.md](.planning/active.md) (1) + * `* TODO prioritise` * [game/ai/src/decision.rs](game/ai/src/decision.rs) (1) * `// TODO use a simpler manual vec that doesnt run destructors` * [game/ai/src/intelligence.rs](game/ai/src/intelligence.rs) (10) @@ -228,6 +230,9 @@ * `// TODO might be better to just insert sorted` * [game/simulation/src/input/command.rs](game/simulation/src/input/command.rs) (1) * `// TODO expand/contract in a direction` + * [game/simulation/src/input/popup.rs](game/simulation/src/input/popup.rs) (2) + * `// TODO bump alloc` + * `// TODO` * [game/simulation/src/input/system.rs](game/simulation/src/input/system.rs) (3) * `// TODO multiple clicks in the same place should iterate through all entities in selection range` * `// TODO spatial lookup for ui elements too` @@ -492,7 +497,7 @@ * `// TODO use instances or indices?` * `let _no_depth = Capability::DepthTest.scoped_disable(); // TODO clear depth mask instead` * [renderer/engine/src/render/sdl/ui/render.rs](renderer/engine/src/render/sdl/ui/render.rs) (1) - * `// TODO actual popup content` + * `// TODO render buttons` * [renderer/engine/src/render/sdl/ui/windows/debug_renderer.rs](renderer/engine/src/render/sdl/ui/windows/debug_renderer.rs) (1) * `// TODO proper default script path` * [renderer/engine/src/render/sdl/ui/windows/selection.rs](renderer/engine/src/render/sdl/ui/windows/selection.rs) (9) diff --git a/game/simulation/src/ecs/world.rs b/game/simulation/src/ecs/world.rs index 5a847367..47e1d3c6 100644 --- a/game/simulation/src/ecs/world.rs +++ b/game/simulation/src/ecs/world.rs @@ -548,6 +548,12 @@ impl<'a, T: Component + Debug> Debug for ComponentRef<'a, T> { } } +impl<'a, T: Component + Display> Display for ComponentRef<'a, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + Display::fmt(self.comp, f) + } +} + impl Deref for ComponentRefMapped<'_, T, U> { type Target = U; diff --git a/game/simulation/src/input/popup.rs b/game/simulation/src/input/popup.rs index 6ec4ba38..5f14aafc 100644 --- a/game/simulation/src/input/popup.rs +++ b/game/simulation/src/input/popup.rs @@ -1,25 +1,42 @@ -use crate::Entity; +use crate::input::popup::content::{Button, EntityContent, TileSelectionContent}; +use crate::{EcsWorld, Entity}; /// Single right click context menu #[derive(Default)] pub struct UiPopup { popup: Option, - open: bool, } pub struct PreparedUiPopup<'a>(&'a mut UiPopup); -#[derive(Debug, Clone)] -pub enum PopupContent { +#[derive(Copy, Clone)] +pub enum PopupContentType { TileSelection, Entity(Entity), } +pub struct PopupContent { + ty: PopupContentType, + content: Option, +} + +// TODO bump alloc +pub struct RenderedPopupContent { + title: String, + buttons: Vec