From b50645c1ee684b82b92f4810ca2a45731c4462fa Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 10 Apr 2023 17:59:43 +0100 Subject: [PATCH] Addressed review comments --- Cargo.lock | 1 - client/src/lib.rs | 2 +- common/Cargo.toml | 1 - common/src/character.rs | 2 +- common/src/cmd.rs | 2 +- common/state/src/lib.rs | 2 +- common/state/src/state.rs | 38 ++++++++---- rtsim/src/ai/mod.rs | 5 +- rtsim/src/data/mod.rs | 37 +++++++++++- rtsim/src/data/nature.rs | 2 + rtsim/src/data/npc.rs | 13 +++++ rtsim/src/data/sentiment.rs | 18 +++--- rtsim/src/data/site.rs | 1 + rtsim/src/gen/mod.rs | 3 +- rtsim/src/gen/name.rs | 2 +- rtsim/src/lib.rs | 19 ++++++ rtsim/src/rule/cleanup.rs | 8 +-- rtsim/src/rule/migrate.rs | 26 +++++++-- rtsim/src/rule/npc_ai.rs | 43 ++++++++------ rtsim/src/rule/replenish_resources.rs | 12 ++-- rtsim/src/rule/report.rs | 3 + rtsim/src/rule/simulate_npcs.rs | 22 +++++-- rtsim/src/rule/sync_npcs.rs | 2 +- server/src/cmd.rs | 17 ++++-- server/src/events/entity_creation.rs | 6 +- server/src/lib.rs | 67 +++++++++------------- server/src/rtsim/event.rs | 7 +-- server/src/rtsim/mod.rs | 40 +++++++------ server/src/rtsim/rule/deplete_resources.rs | 49 ++++++++-------- 29 files changed, 286 insertions(+), 164 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05d8a07480..c80aae7065 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6710,7 +6710,6 @@ dependencies = [ "crossbeam-utils 0.8.11", "csv", "dot_vox", - "enum-iterator 1.1.3", "enum-map", "fxhash", "hashbrown 0.12.3", diff --git a/client/src/lib.rs b/client/src/lib.rs index 665f806f36..7432f09998 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1855,7 +1855,7 @@ impl Client { true, None, &self.connected_server_constants, - |_, _, _, _| {}, + |_, _| {}, ); // TODO: avoid emitting these in the first place let _ = self diff --git a/common/Cargo.toml b/common/Cargo.toml index 8ff5bd5ee9..8aff837d6b 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -26,7 +26,6 @@ common-base = { package = "veloren-common-base", path = "base" } serde = { version = "1.0.110", features = ["derive", "rc"] } # Util -enum-iterator = "1.1.3" enum-map = "2.4" vek = { version = "0.15.8", features = ["serde"] } cfg-if = "1.0.0" diff --git a/common/src/character.rs b/common/src/character.rs index 576d5a868a..5b8bdd9501 100644 --- a/common/src/character.rs +++ b/common/src/character.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; /// The limit on how many characters that a player can have pub const MAX_CHARACTERS_PER_PLAYER: usize = 8; -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] #[serde(transparent)] pub struct CharacterId(pub i64); diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 94e025e300..4fb54e7434 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -659,7 +659,7 @@ impl ServerChatCommand { Enum("entity", ENTITIES.clone(), Required), Integer("amount", 1, Optional), Boolean("ai", "true".to_string(), Optional), - Float("ai", 1.0, Optional), + Float("scale", 1.0, Optional), ], "Spawn a test entity", Some(Admin), diff --git a/common/state/src/lib.rs b/common/state/src/lib.rs index d0047fe6de..34025380bf 100644 --- a/common/state/src/lib.rs +++ b/common/state/src/lib.rs @@ -6,4 +6,4 @@ mod build_areas; mod state; // TODO: breakup state module and remove glob pub use build_areas::{BuildAreaError, BuildAreas}; -pub use state::{BlockChange, State, TerrainChanges}; +pub use state::{BlockChange, BlockDiff, State, TerrainChanges}; diff --git a/common/state/src/state.rs b/common/state/src/state.rs index db0d1f834c..20224a5748 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -113,6 +113,13 @@ impl TerrainChanges { } } +#[derive(Clone)] +pub struct BlockDiff { + pub wpos: Vec3, + pub old: Block, + pub new: Block, +} + /// A type used to represent game state stored on both the client and the /// server. This includes things like entity components, terrain data, and /// global states like weather, time of day, etc. @@ -525,10 +532,7 @@ impl State { } // Apply terrain changes - pub fn apply_terrain_changes( - &self, - block_update: impl FnMut(&specs::World, Vec3, Block, Block), - ) { + pub fn apply_terrain_changes(&self, block_update: impl FnMut(&specs::World, Vec)) { self.apply_terrain_changes_internal(false, block_update); } @@ -543,7 +547,7 @@ impl State { fn apply_terrain_changes_internal( &self, during_tick: bool, - mut block_update: impl FnMut(&specs::World, Vec3, Block, Block), + mut block_update: impl FnMut(&specs::World, Vec), ) { span!( _guard, @@ -585,20 +589,30 @@ impl State { } // Apply block modifications // Only include in `TerrainChanges` if successful - modified_blocks.retain(|pos, new_block| { - let res = terrain.map(*pos, |old_block| { - block_update(&self.ecs, *pos, old_block, *new_block); - *new_block + let mut updated_blocks = Vec::with_capacity(modified_blocks.len()); + modified_blocks.retain(|wpos, new| { + let res = terrain.map(*wpos, |old| { + updated_blocks.push(BlockDiff { + wpos: *wpos, + old, + new: *new, + }); + *new }); - if let (&Ok(old_block), true) = (&res, during_tick) { + if let (&Ok(old), true) = (&res, during_tick) { // NOTE: If the changes are applied during the tick, we push the *old* value as // the modified block (since it otherwise can't be recovered after the tick). // Otherwise, the changes will be applied after the tick, so we push the *new* // value. - *new_block = old_block; + *new = old; } res.is_ok() }); + + if !updated_blocks.is_empty() { + block_update(&self.ecs, updated_blocks); + } + self.ecs.write_resource::().modified_blocks = modified_blocks; } @@ -610,7 +624,7 @@ impl State { update_terrain_and_regions: bool, mut metrics: Option<&mut StateTickMetrics>, server_constants: &ServerConstants, - block_update: impl FnMut(&specs::World, Vec3, Block, Block), + block_update: impl FnMut(&specs::World, Vec), ) { span!(_guard, "tick", "State::tick"); diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index c9b2c4ecf2..2fe6222151 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -184,6 +184,9 @@ pub trait Action: Any + Send + Sync { /// want to return one of many actions (each with different types) from /// the same function. /// + /// Note that [`Either`] can often be used to unify mismatched types without + /// the need for boxing. + /// /// # Example /// /// ```ignore @@ -569,7 +572,7 @@ where /// The inner function will be run every tick to decide on an action. When an /// action is chosen, it will be performed until completed unless a different /// action of the same or higher priority is chosen in a subsequent tick. -/// [`watch`] is very unfocussed and will happily switch between actions +/// [`watch`] is very unfocused and will happily switch between actions /// rapidly between ticks if conditions change. If you want something that /// tends to commit to actions until they are completed, see [`choose`]. /// diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index 75252cd5b7..eb4e0748a9 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -24,8 +24,19 @@ use std::{ marker::PhantomData, }; +/// The current version of rtsim data. +/// +/// Note that this number does *not* need incrementing on every change: most +/// field removals/additions are fine. This number should only be incremented +/// when we wish to perform a *hard purge* of rtsim data. +pub const CURRENT_VERSION: u32 = 0; + #[derive(Clone, Serialize, Deserialize)] pub struct Data { + // Absence of field just implied version = 0 + #[serde(default)] + pub version: u32, + pub nature: Nature, #[serde(default)] pub npcs: Npcs, @@ -46,7 +57,21 @@ pub struct Data { pub should_purge: bool, } -pub type ReadError = rmp_serde::decode::Error; +pub enum ReadError { + Load(rmp_serde::decode::Error), + // Preserve old data + VersionMismatch(Box), +} + +impl fmt::Debug for ReadError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Load(err) => err.fmt(f), + Self::VersionMismatch(_) => write!(f, "VersionMismatch"), + } + } +} + pub type WriteError = rmp_serde::encode::Error; impl Data { @@ -59,8 +84,16 @@ impl Data { id } - pub fn from_reader(reader: R) -> Result { + pub fn from_reader(reader: R) -> Result, ReadError> { rmp_serde::decode::from_read(reader) + .map_err(ReadError::Load) + .and_then(|data: Data| { + if data.version == CURRENT_VERSION { + Ok(Box::new(data)) + } else { + Err(ReadError::VersionMismatch(Box::new(data))) + } + }) } pub fn write_to(&self, mut writer: W) -> Result<(), WriteError> { diff --git a/rtsim/src/data/nature.rs b/rtsim/src/data/nature.rs index 0e4b39f4f7..cece7ec555 100644 --- a/rtsim/src/data/nature.rs +++ b/rtsim/src/data/nature.rs @@ -55,6 +55,8 @@ pub struct Chunk { /// generation. This value represents only the variable 'depletion' factor /// of that resource, which shall change over time as the world evolves /// and players interact with it. + // TODO: Consider whether we can use `i16` or similar here instead: `f32` has more resolution + // than we might need. #[serde(rename = "r")] #[serde(serialize_with = "crate::data::rugged_ser_enum_map::<_, _, _, 1>")] #[serde(deserialize_with = "crate::data::rugged_de_enum_map::<_, _, _, 1>")] diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index fbefd49a47..9e01043373 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -179,21 +179,25 @@ impl Npc { } } + // TODO: have a dedicated `NpcBuilder` type for this. pub fn with_personality(mut self, personality: Personality) -> Self { self.personality = personality; self } + // TODO: have a dedicated `NpcBuilder` type for this. pub fn with_profession(mut self, profession: impl Into>) -> Self { self.profession = profession.into(); self } + // TODO: have a dedicated `NpcBuilder` type for this. pub fn with_home(mut self, home: impl Into>) -> Self { self.home = home.into(); self } + // TODO: have a dedicated `NpcBuilder` type for this. pub fn steering(mut self, vehicle: impl Into>) -> Self { self.riding = vehicle.into().map(|vehicle| Riding { vehicle, @@ -202,6 +206,7 @@ impl Npc { self } + // TODO: have a dedicated `NpcBuilder` type for this. pub fn riding(mut self, vehicle: impl Into>) -> Self { self.riding = vehicle.into().map(|vehicle| Riding { vehicle, @@ -210,6 +215,7 @@ impl Npc { self } + // TODO: have a dedicated `NpcBuilder` type for this. pub fn with_faction(mut self, faction: impl Into>) -> Self { self.faction = faction.into(); self @@ -217,10 +223,14 @@ impl Npc { pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed.wrapping_add(perm)) } + // TODO: Don't make this depend on deterministic RNG, actually persist names + // once we've decided that we want to pub fn get_name(&self) -> String { name::generate(&mut self.rng(Self::PERM_NAME)) } pub fn cleanup(&mut self, reports: &Reports) { // Clear old or superfluous sentiments + // TODO: It might be worth giving more important NPCs a higher sentiment + // 'budget' than less important ones. self.sentiments .cleanup(crate::data::sentiment::NPC_MAX_SENTIMENTS); // Clear reports that have been forgotten @@ -305,6 +315,7 @@ pub struct Npcs { pub npcs: HopSlotMap, pub vehicles: HopSlotMap, // TODO: This feels like it should be its own rtsim resource + // TODO: Consider switching to `common::util::SpatialGrid` instead #[serde(skip, default = "construct_npc_grid")] pub npc_grid: Grid, #[serde(skip)] @@ -332,6 +343,8 @@ impl Npcs { } /// Queries nearby npcs, not garantueed to work if radius > 32.0 + // TODO: Find a more efficient way to implement this, it's currently + // (theoretically) O(n^2). pub fn nearby( &self, this_npc: Option, diff --git a/rtsim/src/data/sentiment.rs b/rtsim/src/data/sentiment.rs index 30ed3be98a..30a59e4ab8 100644 --- a/rtsim/src/data/sentiment.rs +++ b/rtsim/src/data/sentiment.rs @@ -5,6 +5,7 @@ use common::{ use hashbrown::HashMap; use rand::prelude::*; use serde::{Deserialize, Serialize}; +use std::collections::BinaryHeap; // Factions have a larger 'social memory' than individual NPCs and so we allow // them to have more sentiments @@ -22,7 +23,7 @@ const DECAY_TIME_FACTOR: f32 = 1.0; //6.0; TODO: Use this value when we're happy // - Occupations (hatred of hunters or chefs?) // - Ideologies (dislikes democracy, likes monarchy?) // - etc. -#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)] pub enum Target { Character(CharacterId), Npc(NpcId), @@ -105,13 +106,12 @@ impl Sentiments { // For each sentiment, calculate how valuable it is for us to remember. // For now, we just use the absolute value of the sentiment but later on we might want to favour // sentiments toward factions and other 'larger' groups over, say, sentiments toward players/other NPCs - .map(|(tgt, sentiment)| (*tgt, sentiment.positivity.unsigned_abs())) - .collect::>(); - sentiments.sort_unstable_by_key(|(_, value)| *value); + .map(|(tgt, sentiment)| (sentiment.positivity.unsigned_abs(), *tgt)) + .collect::>(); // Remove the superfluous sentiments - for (tgt, _) in &sentiments[0..self.map.len() - max_sentiments] { - self.map.remove(tgt); + for (_, tgt) in sentiments.drain().take(self.map.len() - max_sentiments) { + self.map.remove(&tgt); } } } @@ -156,7 +156,7 @@ impl Sentiment { /// generally try to harm the actor in any way they can. pub const VILLAIN: f32 = -0.8; - fn value(&self) -> f32 { self.positivity as f32 / 126.0 } + fn value(&self) -> f32 { self.positivity as f32 * (1.0 / 126.0) } fn change_by(&mut self, change: f32, cap: f32) { // There's a bit of ceremony here for two reasons: @@ -175,8 +175,8 @@ impl Sentiment { fn decay(&mut self, rng: &mut impl Rng, dt: f32) { if self.positivity != 0 { - // TODO: Make dt-independent so we can slow tick rates - // 36 = 6 * 6 + // TODO: Find a slightly nicer way to have sentiment decay, perhaps even by + // remembering the last interaction instead of constant updates. if rng.gen_bool( (1.0 / (self.positivity.unsigned_abs() as f32 * DECAY_TIME_FACTOR.powi(2) * dt)) as f64, diff --git a/rtsim/src/data/site.rs b/rtsim/src/data/site.rs index 9f02d41a92..c736e49623 100644 --- a/rtsim/src/data/site.rs +++ b/rtsim/src/data/site.rs @@ -37,6 +37,7 @@ pub struct Site { #[serde(skip_serializing, skip_deserializing)] pub world_site: Option>, + // Note: there's currently no guarantee that site populations are non-intersecting #[serde(skip_serializing, skip_deserializing)] pub population: HashSet, } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 0885d0e6cb..1c6c8c13ce 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -6,7 +6,7 @@ use crate::data::{ faction::Faction, npc::{Npc, Npcs, Profession, Vehicle}, site::Site, - Data, Nature, + Data, Nature, CURRENT_VERSION, }; use common::{ comp::{self, Body}, @@ -30,6 +30,7 @@ impl Data { let mut rng = SmallRng::from_seed(seed); let mut this = Self { + version: CURRENT_VERSION, nature: Nature::generate(world), npcs: Npcs { npcs: Default::default(), diff --git a/rtsim/src/gen/name.rs b/rtsim/src/gen/name.rs index 736bb34dd5..4c56387804 100644 --- a/rtsim/src/gen/name.rs +++ b/rtsim/src/gen/name.rs @@ -9,7 +9,7 @@ pub fn generate(rng: &mut impl Rng) -> String { name += starts.choose(rng).unwrap(); - for _ in 0..thread_rng().gen_range(1..=3) { + for _ in 0..rng.gen_range(1..=3) { name += vowels.choose(rng).unwrap(); name += cons.choose(rng).unwrap(); } diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index add27a17cd..529fdf157f 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -91,6 +91,9 @@ impl RtState { .borrow_mut() } + // TODO: Consider whether it's worth explicitly calling rule event handlers + // instead of allowing them to bind event handlers. Less modular, but + // potentially easier to deal with data dependencies? pub fn bind( &mut self, f: impl FnMut(EventCtx) + Send + Sync + 'static, @@ -114,6 +117,8 @@ impl RtState { pub fn data_mut(&self) -> impl DerefMut + '_ { self.resource_mut() } + pub fn get_data_mut(&mut self) -> &mut Data { self.get_resource_mut() } + pub fn resource(&self) -> impl Deref + '_ { self.resources .get::>() @@ -126,6 +131,18 @@ impl RtState { .borrow() } + pub fn get_resource_mut(&mut self) -> &mut R { + self.resources + .get_mut::>() + .unwrap_or_else(|| { + panic!( + "Tried to access resource '{}' but it does not exist", + type_name::() + ) + }) + .get_mut() + } + pub fn resource_mut(&self) -> impl DerefMut + '_ { self.resources .get::>() @@ -139,6 +156,8 @@ impl RtState { } pub fn emit(&mut self, e: E, world: &World, index: IndexRef) { + // TODO: Queue these events up and handle them on a regular rtsim tick instead + // of executing their handlers immediately. if let Some(handlers) = self.event_handlers.get::>() { handlers.iter().for_each(|f| f(self, world, index, &e)); } diff --git a/rtsim/src/rule/cleanup.rs b/rtsim/src/rule/cleanup.rs index c661277433..a9e2d5e1ff 100644 --- a/rtsim/src/rule/cleanup.rs +++ b/rtsim/src/rule/cleanup.rs @@ -20,13 +20,13 @@ impl Rule for CleanUp { let data = &mut *ctx.state.data_mut(); let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()); - for (_, npc) in data.npcs + // TODO: Use `.into_par_iter()` for these by implementing rayon traits in upstream slotmap. + + data.npcs .iter_mut() // Only cleanup NPCs every few ticks .filter(|(_, npc)| (npc.seed as u64 + ctx.event.tick) % NPC_SENTIMENT_TICK_SKIP == 0) - { - npc.sentiments.decay(&mut rng, ctx.event.dt * NPC_SENTIMENT_TICK_SKIP as f32); - } + .for_each(|(_, npc)| npc.sentiments.decay(&mut rng, ctx.event.dt * NPC_SENTIMENT_TICK_SKIP as f32)); // Clean up entities data.npcs diff --git a/rtsim/src/rule/migrate.rs b/rtsim/src/rule/migrate.rs index a81b56b92b..2153fb6131 100644 --- a/rtsim/src/rule/migrate.rs +++ b/rtsim/src/rule/migrate.rs @@ -2,6 +2,7 @@ use crate::{data::Site, event::OnSetup, RtState, Rule, RuleError}; use rand::prelude::*; use rand_chacha::ChaChaRng; use tracing::warn; +use world::site::SiteKind; /// This rule runs at rtsim startup and broadly acts to perform some primitive /// migration/sanitisation in order to ensure that the state of rtsim is mostly @@ -36,11 +37,6 @@ impl Rule for Migrate { } }); - for npc in data.npcs.values_mut() { - // TODO: Consider what to do with homeless npcs. - npc.home = npc.home.filter(|home| data.sites.contains_key(*home)); - } - // Generate rtsim sites for world sites that don't have a corresponding rtsim // site yet for (world_site_id, _) in ctx.index.sites.iter() { @@ -65,7 +61,25 @@ impl Rule for Migrate { } } - // TODO: Reassign sites for NPCs if they don't have one + // Reassign NPCs to sites if their old one was deleted. If they were already homeless, no need to do anything. + for npc in data.npcs.values_mut() { + if let Some(home) = npc.home + && !data.sites.contains_key(home) + { + // Choose the closest habitable site as the new home for the NPC + npc.home = data.sites.sites + .iter() + .filter(|(_, site)| { + // TODO: This is a bit silly, but needs to wait on the removal of site1 + site.world_site.map_or(false, |ws| matches!(&ctx.index.sites.get(ws).kind, SiteKind::Refactor(_) + | SiteKind::CliffTown(_) + | SiteKind::SavannahPit(_) + | SiteKind::DesertCity(_))) + }) + .min_by_key(|(_, site)| site.wpos.as_().distance_squared(npc.wpos.xy()) as i32) + .map(|(site_id, _)| site_id); + } + } }); Ok(Self) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 30f240b3ff..21ad79b42c 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -469,24 +469,27 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { fn socialize() -> impl Action { now(|ctx| { // TODO: Bit odd, should wait for a while after greeting - if ctx.rng.gen_bool(0.002) && let Some(other) = ctx - .state - .data() - .npcs - .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0) - .choose(&mut ctx.rng) - { - just(move |ctx| ctx.controller.say(other, "npc-speech-villager_open")).boxed() - } else if ctx.rng.gen_bool(0.0003) { - just(|ctx| ctx.controller.do_dance()) - .repeat() - .stop_if(timeout(6.0)) - .debug(|| "dancing") - .map(|_| ()) - .boxed() - } else { - idle().boxed() + if ctx.rng.gen_bool(0.002) { + if ctx.rng.gen_bool(0.15) { + return just(|ctx| ctx.controller.do_dance()) + .repeat() + .stop_if(timeout(6.0)) + .debug(|| "dancing") + .map(|_| ()) + .boxed(); + } else if let Some(other) = ctx + .state + .data() + .npcs + .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0) + .choose(&mut ctx.rng) + { + return just(move |ctx| ctx.controller.say(other, "npc-speech-villager_open")) + .boxed(); + } } + + idle().boxed() }) } @@ -914,6 +917,12 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option { } fn check_for_enemies(ctx: &mut NpcCtx) -> Option { + // TODO: Instead of checking all nearby actors every tick, it would be more + // effective to have the actor grid generate a per-tick diff so that we only + // need to check new actors in the local area. Be careful though: + // implementing this means accounting for changes in sentiment (that could + // suddenly make a nearby actor an enemy) as well as variable NPC tick + // rates! ctx.state .data() .npcs diff --git a/rtsim/src/rule/replenish_resources.rs b/rtsim/src/rule/replenish_resources.rs index 3aff5256e9..b2f3c0969d 100644 --- a/rtsim/src/rule/replenish_resources.rs +++ b/rtsim/src/rule/replenish_resources.rs @@ -9,7 +9,11 @@ pub struct ReplenishResources; // TODO: Non-renewable resources? pub const REPLENISH_TIME: f32 = 60.0 * 60.0; /// How many chunks should be replenished per tick? -pub const REPLENISH_PER_TICK: usize = 100000; +// TODO: It should be possible to optimise this be remembering the last +// modification time for each chunk, then lazily projecting forward using a +// closed-form solution to the replenishment to calculate resources in a lazy +// manner. +pub const REPLENISH_PER_TICK: usize = 8192; impl Rule for ReplenishResources { fn start(rtstate: &mut RtState) -> Result { @@ -19,9 +23,9 @@ impl Rule for ReplenishResources { // How much should be replenished for each chosen chunk to hit our target // replenishment rate? - let replenish_amount = world_size.product() as f32 * ctx.event.dt - / REPLENISH_TIME - / REPLENISH_PER_TICK as f32; + let replenish_amount = world_size.product() as f32 + * ctx.event.dt + * (1.0 / REPLENISH_TIME / REPLENISH_PER_TICK as f32); for _ in 0..REPLENISH_PER_TICK { let key = world_size.map(|e| thread_rng().gen_range(0..e as i32)); diff --git a/rtsim/src/rule/report.rs b/rtsim/src/rule/report.rs index 36496796b1..74874a2605 100644 --- a/rtsim/src/rule/report.rs +++ b/rtsim/src/rule/report.rs @@ -33,6 +33,9 @@ fn on_death(ctx: EventCtx) { at: data.time_of_day, }); + // TODO: Don't push report to NPC inboxes, have a dedicated data structure that + // tracks reports by chunks and then have NPCs decide to query this + // data structure in their own time. for npc_id in nearby { if let Some(npc) = data.npcs.get_mut(npc_id) { npc.inbox.push_back(report); diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index f35dc3a215..cdd25d1cca 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -52,7 +52,7 @@ fn on_death(ctx: EventCtx) { let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()); // Respawn dead NPCs - match npc.body { + let details = match npc.body { Body::Humanoid(_) => { if let Some((site_id, site)) = data .sites @@ -76,15 +76,17 @@ fn on_death(ctx: EventCtx) { let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap(); Body::Humanoid(comp::humanoid::Body::random_with(rng, species)) }; - data.spawn_npc( + let npc_id = data.spawn_npc( Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) .with_personality(Personality::random(&mut rng)) .with_home(site_id) .with_faction(npc.faction) .with_profession(npc.profession.clone()), ); + Some((npc_id, site_id)) } else { warn!("No site found for respawning humaniod"); + None } }, Body::BirdLarge(_) => { @@ -112,7 +114,7 @@ fn on_death(ctx: EventCtx) { ] .choose(&mut rng) .unwrap(); - data.npcs.create_npc( + let npc_id = data.npcs.create_npc( Npc::new( rng.gen(), rand_wpos(&mut rng), @@ -122,11 +124,23 @@ fn on_death(ctx: EventCtx) { ) .with_home(site_id), ); + Some((npc_id, site_id)) } else { warn!("No site found for respawning bird"); + None } }, - _ => unimplemented!(), + body => { + error!("Tried to respawn rtsim NPC with invalid body: {:?}", body); + None + }, + }; + + // Add the NPC to their home site + if let Some((npc_id, home_site)) = details { + if let Some(home) = data.sites.get_mut(home_site) { + home.population.insert(npc_id); + } } } } diff --git a/rtsim/src/rule/sync_npcs.rs b/rtsim/src/rule/sync_npcs.rs index 4400612748..26464de8b3 100644 --- a/rtsim/src/rule/sync_npcs.rs +++ b/rtsim/src/rule/sync_npcs.rs @@ -22,7 +22,7 @@ fn on_setup(ctx: EventCtx) { // Create NPC grid data.npcs.npc_grid = Grid::new(ctx.world.sim().get_size().as_(), Default::default()); - // Add NPCs to home population (TODO: Do this on entity creation?) + // Add NPCs to home population for (npc_id, npc) in data.npcs.npcs.iter() { if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) { home.population.insert(npc_id); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index da2b46ce45..9f38f0f046 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1329,6 +1329,8 @@ fn handle_rtsim_npc( } } +// TODO: Remove this command when rtsim becomes more mature and we're sure we +// don't need purges to fix broken state. fn handle_rtsim_purge( server: &mut Server, client: EcsEntity, @@ -1337,6 +1339,14 @@ fn handle_rtsim_purge( action: &ServerChatCommand, ) -> CmdResult<()> { use crate::rtsim::RtSim; + let client_uuid = uuid(server, client, "client")?; + if !matches!(real_role(server, client_uuid, "client")?, AdminRole::Admin) { + return Err( + "You must be a real admin (not just a temporary admin) to purge rtsim data." + .to_string(), + ); + } + if let Some(should_purge) = parse_cmd_args!(args, bool) { server .state @@ -2082,6 +2092,7 @@ fn handle_kill_npcs( let healths = ecs.write_storage::(); let players = ecs.read_storage::(); let alignments = ecs.read_storage::(); + let rtsim_entities = ecs.read_storage::(); ( &entities, @@ -2101,11 +2112,7 @@ fn handle_kill_npcs( }; if should_kill { - if let Some(rtsim_entity) = ecs - .read_storage::() - .get(entity) - .copied() - { + if let Some(rtsim_entity) = rtsim_entities.get(entity).copied() { ecs.write_resource::() .hook_rtsim_actor_death( &ecs.read_resource::>(), diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index d28b392a73..9441604606 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -195,7 +195,7 @@ pub fn handle_create_ship( ship: comp::ship::Body, rtsim_vehicle: Option, driver: Option, - passangers: Vec, + passengers: Vec, ) { let mut entity = server .state @@ -234,8 +234,8 @@ pub fn handle_create_ship( } } - for passanger in passangers { - handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passanger); + for passenger in passengers { + handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passenger); } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 96b8baee2f..e47d3e412f 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -80,7 +80,7 @@ use common::{ rtsim::{RtSimEntity, RtSimVehicle}, shared_server_config::ServerConstants, slowjob::SlowJobPool, - terrain::{Block, TerrainChunk, TerrainChunkSize}, + terrain::{TerrainChunk, TerrainChunkSize}, vol::RectRasterableVol, }; use common_ecs::run_now; @@ -88,7 +88,7 @@ use common_net::{ msg::{ClientType, DisconnectReason, ServerGeneral, ServerInfo, ServerMsg}, sync::WorldSyncExt, }; -use common_state::{BuildAreas, State}; +use common_state::{BlockDiff, BuildAreas, State}; use common_systems::add_local_systems; use metrics::{EcsSystemMetrics, PhysicsMetrics, TickMetrics}; use network::{ListenAddr, Network, Pid}; @@ -698,21 +698,15 @@ impl Server { let before_state_tick = Instant::now(); - fn on_block_update( - ecs: &specs::World, - wpos: Vec3, - old_block: Block, - new_block: Block, - ) { + fn on_block_update(ecs: &specs::World, changes: Vec) { // When a resource block updates, inform rtsim - if old_block.get_rtsim_resource().is_some() || new_block.get_rtsim_resource().is_some() - { + if changes.iter().any(|c| { + c.old.get_rtsim_resource().is_some() || c.new.get_rtsim_resource().is_some() + }) { ecs.write_resource::().hook_block_update( &ecs.read_resource::>(), ecs.read_resource::().as_index_ref(), - wpos, - old_block, - new_block, + changes, ); } } @@ -838,35 +832,26 @@ impl Server { .collect::>() }; - for entity in to_delete { - // Assimilate entities that are part of the real-time world simulation - #[cfg(feature = "worldgen")] - if let Some(rtsim_entity) = self - .state - .ecs() - .read_storage::() - .get(entity) - .copied() - { - self.state - .ecs() - .write_resource::() - .hook_rtsim_entity_unload(rtsim_entity); - } - #[cfg(feature = "worldgen")] - if let Some(rtsim_vehicle) = self - .state - .ecs() - .read_storage::() - .get(entity) - .copied() - { - self.state - .ecs() - .write_resource::() - .hook_rtsim_vehicle_unload(rtsim_vehicle); - } + { + let mut rtsim = self.state.ecs().write_resource::(); + let rtsim_entities = self.state.ecs().read_storage::(); + let rtsim_vehicles = self.state.ecs().read_storage::(); + // Assimilate entities that are part of the real-time world simulation + for entity in &to_delete { + #[cfg(feature = "worldgen")] + if let Some(rtsim_entity) = rtsim_entities.get(*entity) { + rtsim.hook_rtsim_entity_unload(*rtsim_entity); + } + #[cfg(feature = "worldgen")] + if let Some(rtsim_vehicle) = rtsim_vehicles.get(*entity) { + rtsim.hook_rtsim_vehicle_unload(*rtsim_vehicle); + } + } + } + + // Actually perform entity deletion + for entity in to_delete { if let Err(e) = self.state.delete_entity_recorded(entity) { error!(?e, "Failed to delete agent outside the terrain"); } diff --git a/server/src/rtsim/event.rs b/server/src/rtsim/event.rs index 50261c98ab..0a15a3a2de 100644 --- a/server/src/rtsim/event.rs +++ b/server/src/rtsim/event.rs @@ -1,12 +1,9 @@ -use common::terrain::Block; +use common_state::BlockDiff; use rtsim::Event; -use vek::*; #[derive(Clone)] pub struct OnBlockChange { - pub wpos: Vec3, - pub old: Block, - pub new: Block, + pub changes: Vec, } impl Event for OnBlockChange {} diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index 416c8f2598..27ad649538 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -6,13 +6,13 @@ use atomicwrites::{AtomicFile, OverwriteBehavior}; use common::{ grid::Grid, rtsim::{Actor, ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings}, - terrain::Block, }; use common_ecs::dispatch; +use common_state::BlockDiff; use crossbeam_channel::{unbounded, Receiver, Sender}; use enum_map::EnumMap; use rtsim::{ - data::{npc::SimulationMode, Data}, + data::{npc::SimulationMode, Data, ReadError}, event::{OnDeath, OnSetup}, RtState, }; @@ -51,8 +51,17 @@ impl RtSim { match File::open(&file_path) { Ok(file) => { info!("Rtsim data found. Attempting to load..."); + + let ignore_version = std::env::var("RTSIM_IGNORE_VERSION").is_ok(); + match Data::from_reader(io::BufReader::new(file)) { - Ok(data) => { + Err(ReadError::VersionMismatch(_)) if !ignore_version => { + warn!( + "Rtsim data version mismatch (implying a breaking change), \ + rtsim data will be purged" + ); + }, + Ok(data) | Err(ReadError::VersionMismatch(data)) => { info!("Rtsim data loaded."); if data.should_purge { warn!( @@ -60,11 +69,11 @@ impl RtSim { generating afresh" ); } else { - break 'load data; + break 'load *data; } }, - Err(e) => { - error!("Rtsim data failed to load: {}", e); + Err(ReadError::Load(err)) => { + error!("Rtsim data failed to load: {}", err); info!("Old rtsim data will now be moved to a backup file"); let mut i = 0; loop { @@ -139,37 +148,30 @@ impl RtSim { } pub fn hook_load_chunk(&mut self, key: Vec2, max_res: EnumMap) { - if let Some(chunk_state) = self.state.resource_mut::().0.get_mut(key) { + if let Some(chunk_state) = self.state.get_resource_mut::().0.get_mut(key) { *chunk_state = Some(LoadedChunkState { max_res }); } } pub fn hook_unload_chunk(&mut self, key: Vec2) { - if let Some(chunk_state) = self.state.resource_mut::().0.get_mut(key) { + if let Some(chunk_state) = self.state.get_resource_mut::().0.get_mut(key) { *chunk_state = None; } } - pub fn hook_block_update( - &mut self, - world: &World, - index: IndexRef, - wpos: Vec3, - old: Block, - new: Block, - ) { + pub fn hook_block_update(&mut self, world: &World, index: IndexRef, changes: Vec) { self.state - .emit(event::OnBlockChange { wpos, old, new }, world, index); + .emit(event::OnBlockChange { changes }, world, index); } pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) { - if let Some(npc) = self.state.data_mut().npcs.get_mut(entity.0) { + if let Some(npc) = self.state.get_data_mut().npcs.get_mut(entity.0) { npc.mode = SimulationMode::Simulated; } } pub fn hook_rtsim_vehicle_unload(&mut self, entity: RtSimVehicle) { - if let Some(vehicle) = self.state.data_mut().npcs.vehicles.get_mut(entity.0) { + if let Some(vehicle) = self.state.get_data_mut().npcs.vehicles.get_mut(entity.0) { vehicle.mode = SimulationMode::Simulated; } } diff --git a/server/src/rtsim/rule/deplete_resources.rs b/server/src/rtsim/rule/deplete_resources.rs index 967a77d1c4..01e05e25d3 100644 --- a/server/src/rtsim/rule/deplete_resources.rs +++ b/server/src/rtsim/rule/deplete_resources.rs @@ -7,32 +7,35 @@ pub struct DepleteResources; impl Rule for DepleteResources { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { - let key = ctx.event.wpos.xy().wpos_to_cpos(); - if let Some(Some(chunk_state)) = ctx.state.resource_mut::().0.get(key) { - let mut chunk_res = ctx.state.data().nature.get_chunk_resources(key); - // Remove resources - if let Some(res) = ctx.event.old.get_rtsim_resource() { - if chunk_state.max_res[res] > 0 { - chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 - 1.0) - .round() - .max(0.0) - / chunk_state.max_res[res] as f32; + let chunk_states = ctx.state.resource::(); + let mut data = ctx.state.data_mut(); + for change in &ctx.event.changes { + let key = change.wpos.xy().wpos_to_cpos(); + if let Some(Some(chunk_state)) = chunk_states.0.get(key) { + let mut chunk_res = data.nature.get_chunk_resources(key); + // Remove resources + if let Some(res) = change.old.get_rtsim_resource() { + if chunk_state.max_res[res] > 0 { + chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 + - 1.0) + .round() + .max(0.0) + / chunk_state.max_res[res] as f32; + } } - } - // Replenish resources - if let Some(res) = ctx.event.new.get_rtsim_resource() { - if chunk_state.max_res[res] > 0 { - chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 + 1.0) - .round() - .max(0.0) - / chunk_state.max_res[res] as f32; + // Replenish resources + if let Some(res) = change.new.get_rtsim_resource() { + if chunk_state.max_res[res] > 0 { + chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 + + 1.0) + .round() + .max(0.0) + / chunk_state.max_res[res] as f32; + } } - } - ctx.state - .data_mut() - .nature - .set_chunk_resources(key, chunk_res); + data.nature.set_chunk_resources(key, chunk_res); + } } });