diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 3c4fcf5099..fcfe4dbd50 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -7,8 +7,10 @@ use common::{ Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId, }, store::Id, + terrain::TerrainChunkSize, vol::RectVolSize, }; +use hashbrown::HashMap; use rand::prelude::*; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; @@ -87,22 +89,22 @@ pub struct Npc { pub personality: Personality, // Unpersisted state - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub chunk_pos: Option>, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub current_site: Option, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub controller: Controller, /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the /// server, loaded corresponds to being within a loaded chunk). When in /// loaded mode, the interactions of the NPC should not be simulated but /// should instead be derived from the game. - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub mode: SimulationMode, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub brain: Option, } @@ -203,13 +205,13 @@ pub struct Vehicle { pub body: comp::ship::Body, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub chunk_pos: Option>, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub driver: Option, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] // TODO: Find a way to detect riders when the vehicle is loaded pub riders: Vec, @@ -217,7 +219,7 @@ pub struct Vehicle { /// the server, loaded corresponds to being within a loaded chunk). When /// in loaded mode, the interactions of the Vehicle should not be /// simulated but should instead be derived from the game. - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub mode: SimulationMode, } @@ -250,7 +252,6 @@ impl Vehicle { #[derive(Default, Clone, Serialize, Deserialize)] pub struct GridCell { pub npcs: Vec, - pub characters: Vec, pub vehicles: Vec, } @@ -258,8 +259,11 @@ pub struct GridCell { pub struct Npcs { pub npcs: HopSlotMap, pub vehicles: HopSlotMap, + // TODO: This feels like it should be its own rtsim resource #[serde(skip, default = "construct_npc_grid")] pub npc_grid: Grid, + #[serde(skip)] + pub character_map: HashMap, Vec<(common::character::CharacterId, Vec3)>>, } fn construct_npc_grid() -> Grid { Grid::new(Vec2::zero(), Default::default()) } @@ -273,8 +277,11 @@ impl Npcs { /// Queries nearby npcs, not garantueed to work if radius > 32.0 pub fn nearby(&self, wpos: Vec2, radius: f32) -> impl Iterator + '_ { - let chunk_pos = - wpos.as_::() / common::terrain::TerrainChunkSize::RECT_SIZE.as_::(); + let chunk_pos = wpos + .as_::() + .map2(TerrainChunkSize::RECT_SIZE.as_::(), |e, sz| { + e.div_euclid(sz) + }); let r_sqr = radius * radius; LOCALITY .into_iter() @@ -289,19 +296,24 @@ impl Npcs { .map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr) }) .map(Actor::Npc) - .chain(cell.characters - .iter() - .copied() - // TODO: Filter characters by distance too - // .filter(move |npc| { - // self.npcs - // .get(*npc) - // .map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr) - // }) - .map(Actor::Character)) }) }) .flatten() + .chain( + self.character_map + .get(&chunk_pos) + .map(|characters| { + characters.iter().filter_map(move |(character, c_wpos)| { + if c_wpos.xy().distance_squared(wpos) < r_sqr { + Some(Actor::Character(*character)) + } else { + None + } + }) + }) + .into_iter() + .flatten(), + ) } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 19ba5e692b..4f64d34aea 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -34,6 +34,7 @@ impl Data { npcs: Default::default(), vehicles: Default::default(), npc_grid: Grid::new(Vec2::zero(), Default::default()), + character_map: Default::default(), }, sites: Sites { sites: Default::default(), diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 3e36b81355..7bc95ade9c 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -457,7 +457,7 @@ fn socialize() -> impl Action { just(|ctx| { let mut rng = thread_rng(); // TODO: Bit odd, should wait for a while after greeting - if thread_rng().gen_bool(0.0002) { + if thread_rng().gen_bool(0.0003) { if let Some(other) = ctx .state .data() diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 2cb8fef1f6..e39cbed114 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -3,14 +3,15 @@ use super::*; use crate::sys::terrain::NpcData; use common::{ - comp::{self, Body}, + comp::{self, Body, Presence, PresenceKind}, event::{EventBus, NpcBuilder, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time, TimeOfDay}, rtsim::{Actor, RtSimEntity, RtSimVehicle}, slowjob::SlowJobPool, - terrain::CoordinateConversions, + terrain::{CoordinateConversions, TerrainChunkSize}, trade::{Good, SiteInformation}, + vol::RectVolSize, LoadoutBuilder, }; use common_ecs::{Job, Origin, Phase, System}; @@ -187,6 +188,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, RtSimEntity>, ReadStorage<'a, RtSimVehicle>, WriteStorage<'a, comp::Agent>, + ReadStorage<'a, Presence>, ); const NAME: &'static str = "rtsim::tick"; @@ -208,16 +210,53 @@ impl<'a> System<'a> for Sys { rtsim_entities, rtsim_vehicles, mut agents, + presences, ): Self::SystemData, ) { let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; - rtsim.state.data_mut().time_of_day = *time_of_day; + // Set up rtsim inputs + { + let mut data = rtsim.state.data_mut(); + + // Update time of day + data.time_of_day = *time_of_day; + + // Update character map (i.e: so that rtsim knows where players are) + // TODO: Other entities too? Or do we now care about that? + data.npcs.character_map.clear(); + for (character, wpos) in + (&presences, &positions) + .join() + .filter_map(|(presence, pos)| { + if let PresenceKind::Character(character) = &presence.kind { + Some((character, pos.0)) + } else { + None + } + }) + { + let chunk_pos = wpos + .xy() + .as_::() + .map2(TerrainChunkSize::RECT_SIZE.as_::(), |e, sz| { + e.div_euclid(sz) + }); + data.npcs + .character_map + .entry(chunk_pos) + .or_default() + .push((*character, wpos)); + } + } + + // Tick rtsim rtsim .state .tick(&world, index.as_index_ref(), *time_of_day, *time, dt.0); + // Perform a save if required if rtsim .last_saved .map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) @@ -230,6 +269,7 @@ impl<'a> System<'a> for Sys { let chunk_states = rtsim.state.resource::(); let data = &mut *rtsim.state.data_mut(); + // Load in vehicles for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { let chunk = vehicle.wpos.xy().as_::().wpos_to_cpos(); @@ -296,6 +336,7 @@ impl<'a> System<'a> for Sys { } } + // Load in NPCs for (npc_id, npc) in data.npcs.npcs.iter_mut() { let chunk = npc.wpos.xy().as_::().wpos_to_cpos(); diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 9f3924be17..b56ea35fd5 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -478,24 +478,24 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { if let Some(target) = bdata.read_data.lookup_actor(actor) { let target_pos = bdata.read_data.positions.get(target).map(|pos| pos.0); - bdata.agent.target = Some(Target::new( - target, - false, - bdata.read_data.time.0, - false, - target_pos, - )); - if bdata.agent_data.look_toward( &mut bdata.controller, &bdata.read_data, target, ) { + bdata.agent.target = Some(Target::new( + target, + false, + bdata.read_data.time.0, + false, + target_pos, + )); + bdata.controller.push_action(ControlAction::Talk); bdata.controller.push_utterance(UtteranceKind::Greeting); bdata .agent_data - .chat_npc("npc-speech-villager", &mut bdata.event_emitter); + .chat_npc("npc-speech-villager_open", &mut bdata.event_emitter); // Start a timer so that they eventually stop interacting bdata .agent