From 558dd99fd3929dc193f2584ec90547ddf6770d0d Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 11 Aug 2022 21:54:35 +0100 Subject: [PATCH] Added basic rtsim NPC simulation, rtsim controller support --- common/src/rtsim.rs | 8 +- rtsim/src/data/mod.rs | 2 + rtsim/src/data/npc.rs | 89 +++++---- rtsim/src/event.rs | 5 +- rtsim/src/gen/mod.rs | 14 +- rtsim/src/lib.rs | 6 +- rtsim/src/rule/simulate_npcs.rs | 36 +++- server/agent/src/action_nodes.rs | 2 +- server/agent/src/data.rs | 3 +- server/src/rtsim2/tick.rs | 170 +++--------------- server/src/sys/agent.rs | 1 + server/src/sys/agent/behavior_tree.rs | 31 ++-- .../sys/agent/behavior_tree/interaction.rs | 1 - 13 files changed, 139 insertions(+), 229 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 80b541c1c7..3852d09190 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -57,7 +57,7 @@ pub struct RtSimController { /// When this field is `Some(..)`, the agent should attempt to make progress /// toward the given location, accounting for obstacles and other /// high-priority situations like being attacked. - pub travel_to: Option<(Vec3, String)>, + pub travel_to: Option>, /// Proportion of full speed to move pub speed_factor: f32, /// Events @@ -75,12 +75,10 @@ impl Default for RtSimController { } impl RtSimController { - pub fn reset(&mut self) { *self = Self::default(); } - pub fn with_destination(pos: Vec3) -> Self { Self { - travel_to: Some((pos, format!("{:0.1?}", pos))), - speed_factor: 0.25, + travel_to: Some(pos), + speed_factor: 0.5, events: Vec::new(), } } diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index e1a7d66adb..ed6bf14210 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -28,6 +28,8 @@ pub struct Data { pub nature: Nature, pub npcs: Npcs, pub sites: Sites, + + pub time: f64, } pub type ReadError = rmp_serde::decode::Error; diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index c01b3b0081..386a0eb1ff 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -2,50 +2,17 @@ use hashbrown::HashMap; use serde::{Serialize, Deserialize}; use slotmap::HopSlotMap; use vek::*; +use rand::prelude::*; use std::ops::{Deref, DerefMut}; use common::{ uid::Uid, store::Id, - rtsim::SiteId, + rtsim::{SiteId, RtSimController}, + comp, }; +use world::util::RandomPerm; pub use common::rtsim::NpcId; -#[derive(Clone, Serialize, Deserialize)] -pub struct Npc { - // Persisted state - - /// Represents the location of the NPC. - pub loc: NpcLoc, - - // Unpersisted state - - /// The position of the NPC in the world. Note that this is derived from [`Npc::loc`] and cannot be updated manually - #[serde(skip_serializing, skip_deserializing)] - wpos: Vec3, - /// 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)] - pub mode: NpcMode, -} - -impl Npc { - pub fn new(loc: NpcLoc) -> Self { - Self { - loc, - wpos: Vec3::zero(), - mode: NpcMode::Simulated, - } - } - - pub fn wpos(&self) -> Vec3 { self.wpos } - - /// You almost certainly *DO NOT* want to use this method. - /// - /// Update the NPC's wpos as a result of routine NPC simulation derived from its location. - pub(crate) fn tick_wpos(&mut self, wpos: Vec3) { self.wpos = wpos; } -} - #[derive(Copy, Clone, Default)] pub enum NpcMode { /// The NPC is unloaded and is being simulated via rtsim. @@ -56,14 +23,46 @@ pub enum NpcMode { } #[derive(Clone, Serialize, Deserialize)] -pub enum NpcLoc { - Wild { wpos: Vec3 }, - Site { site: SiteId, wpos: Vec3 }, - Travelling { - a: SiteId, - b: SiteId, - frac: f32, - }, +pub struct Npc { + // Persisted state + + /// Represents the location of the NPC. + pub seed: u32, + pub wpos: Vec3, + + // Unpersisted state + + /// (wpos, speed_factor) + #[serde(skip_serializing, skip_deserializing)] + pub target: Option<(Vec3, f32)>, + /// 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)] + pub mode: NpcMode, +} + +impl Npc { + const PERM_SPECIES: u32 = 0; + const PERM_BODY: u32 = 1; + + pub fn new(seed: u32, wpos: Vec3) -> Self { + Self { + seed, + wpos, + target: None, + mode: NpcMode::Simulated, + } + } + + pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed + perm) } + + pub fn get_body(&self) -> comp::Body { + let species = *(&comp::humanoid::ALL_SPECIES) + .choose(&mut self.rng(Self::PERM_SPECIES)) + .unwrap(); + comp::humanoid::Body::random_with(&mut self.rng(Self::PERM_BODY), &species).into() + } } #[derive(Clone, Serialize, Deserialize)] diff --git a/rtsim/src/event.rs b/rtsim/src/event.rs index 298cebf8c8..b60b9825f4 100644 --- a/rtsim/src/event.rs +++ b/rtsim/src/event.rs @@ -17,5 +17,8 @@ pub struct OnSetup; impl Event for OnSetup {} #[derive(Clone)] -pub struct OnTick { pub dt: f32 } +pub struct OnTick { + pub dt: f32, + pub time: f64, +} impl Event for OnTick {} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 4e0877c240..c0a6ba4f94 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -1,7 +1,7 @@ pub mod site; use crate::data::{ - npc::{Npcs, Npc, NpcLoc}, + npc::{Npcs, Npc}, site::{Sites, Site}, Data, Nature, @@ -25,6 +25,8 @@ impl Data { nature: Nature::generate(world), npcs: Npcs { npcs: Default::default() }, sites: Sites { sites: Default::default() }, + + time: 0.0, }; // Register sites with rtsim @@ -39,10 +41,12 @@ impl Data { // Spawn some test entities at the sites for (site_id, site) in this.sites.iter() { - let wpos = site.wpos.map(|e| e as f32) - .with_z(world.sim().get_alt_approx(site.wpos).unwrap_or(0.0)); - this.npcs.create(Npc::new(NpcLoc::Site { site: site_id, wpos })); - println!("Spawned rtsim NPC at {:?}", wpos); + for _ in 0..10 { + let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); + let wpos = wpos2d.map(|e| e as f32 + 0.5) + .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)); + this.npcs.create(Npc::new(rng.gen(), wpos)); + } } this diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 84de682836..5903d7dbaa 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -49,8 +49,8 @@ impl RtState { fn start_default_rules(&mut self) { info!("Starting default rtsim rules..."); - self.start_rule::(); self.start_rule::(); + self.start_rule::(); } pub fn start_rule(&mut self) { @@ -110,6 +110,8 @@ impl RtState { } pub fn tick(&mut self, world: &World, index: IndexRef, dt: f32) { - self.emit(OnTick { dt }, world, index); + self.data_mut().time += dt as f64; + let event = OnTick { dt, time: self.data().time }; + self.emit(event, world, index); } } diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index dae9ed6502..206e3ad5a7 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -1,6 +1,7 @@ use tracing::info; +use vek::*; use crate::{ - data::npc::NpcLoc, + data::npc::NpcMode, event::OnTick, RtState, Rule, RuleError, }; @@ -11,12 +12,33 @@ impl Rule for SimulateNpcs { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { - for (_, npc) in ctx.state.data_mut().npcs.iter_mut() { - npc.tick_wpos(match npc.loc { - NpcLoc::Wild { wpos } => wpos, - NpcLoc::Site { site, wpos } => wpos, - NpcLoc::Travelling { a, b, frac } => todo!(), - }); + for npc in ctx.state + .data_mut() + .npcs + .values_mut() + .filter(|npc| matches!(npc.mode, NpcMode::Simulated)) + { + let body = npc.get_body(); + + if let Some((target, speed_factor)) = npc.target { + npc.wpos += Vec3::from( + (target.xy() - npc.wpos.xy()) + .try_normalized() + .unwrap_or_else(Vec2::zero) + * body.max_speed_approx() + * speed_factor, + ) * ctx.event.dt; + } + } + + // Do some thinking. TODO: Not here! + for npc in ctx.state + .data_mut() + .npcs + .values_mut() + { + // TODO: Not this + npc.target = Some((npc.wpos + Vec3::new(ctx.event.time.sin() as f32 * 16.0, ctx.event.time.cos() as f32 * 16.0, 0.0), 1.0)); } }); diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index cc590e5879..2509c2ac3e 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -212,7 +212,7 @@ impl<'a> AgentData<'a> { } agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0; - if let Some((travel_to, _destination)) = &agent.rtsim_controller.travel_to { + if let Some(travel_to) = &agent.rtsim_controller.travel_to { // If it has an rtsim destination and can fly, then it should. // If it is flying and bumps something above it, then it should move down. if self.traversal_config.can_fly diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index c6a1f18eec..8fc80890b0 100644 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -54,6 +54,7 @@ pub struct AgentData<'a> { pub stance: Option<&'a Stance>, pub cached_spatial_grid: &'a common::CachedSpatialGrid, pub msm: &'a MaterialStatManifest, + pub rtsim_entity: Option<&'a RtSimEntity>, } pub struct TargetData<'a> { @@ -236,7 +237,7 @@ pub struct ReadData<'a> { pub light_emitter: ReadStorage<'a, LightEmitter>, #[cfg(feature = "worldgen")] pub world: ReadExpect<'a, Arc>, - // pub rtsim_entities: ReadStorage<'a, RtSimEntity>, + pub rtsim_entities: ReadStorage<'a, RtSimEntity>, pub buffs: ReadStorage<'a, Buffs>, pub combos: ReadStorage<'a, Combo>, pub active_abilities: ReadStorage<'a, ActiveAbilities>, diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index bcd577594b..55eab5f289 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -8,7 +8,7 @@ use common::{ generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time}, slowjob::SlowJobPool, - rtsim::RtSimEntity, + rtsim::{RtSimEntity, RtSimController}, }; use common_ecs::{Job, Origin, Phase, System}; use rtsim2::data::npc::NpcMode; @@ -26,6 +26,9 @@ impl<'a> System<'a> for Sys { ReadExpect<'a, Arc>, ReadExpect<'a, world::IndexOwned>, ReadExpect<'a, SlowJobPool>, + ReadStorage<'a, comp::Pos>, + ReadStorage<'a, RtSimEntity>, + WriteStorage<'a, comp::Agent>, ); const NAME: &'static str = "rtsim::tick"; @@ -34,7 +37,7 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut Job, - (dt, time, mut server_event_bus, mut rtsim, world, index, slow_jobs): Self::SystemData, + (dt, time, mut server_event_bus, mut rtsim, world, index, slow_jobs, positions, rtsim_entities, mut agents): Self::SystemData, ) { let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; @@ -47,7 +50,7 @@ impl<'a> System<'a> for Sys { let chunk_states = rtsim.state.resource::(); for (npc_id, npc) in rtsim.state.data_mut().npcs.iter_mut() { - let chunk = npc.wpos() + let chunk = npc.wpos .xy() .map2(TerrainChunk::RECT_SIZE, |e, sz| (e as i32).div_euclid(sz as i32)); @@ -55,18 +58,18 @@ impl<'a> System<'a> for Sys { if matches!(npc.mode, NpcMode::Simulated) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) { npc.mode = NpcMode::Loaded; - let body = comp::Body::Object(comp::object::Body::Scarecrow); + let body = npc.get_body(); emitter.emit(ServerEvent::CreateNpc { - pos: comp::Pos(npc.wpos()), + pos: comp::Pos(npc.wpos), stats: comp::Stats::new("Rtsim NPC".to_string()), skill_set: comp::SkillSet::default(), health: None, poise: comp::Poise::new(body), inventory: comp::Inventory::with_empty(), body, - agent: None, + agent: Some(comp::Agent::from_body(&body)), alignment: comp::Alignment::Wild, - scale: comp::Scale(10.0), + scale: comp::Scale(1.0), anchor: None, loot: Default::default(), rtsim_entity: Some(RtSimEntity(npc_id)), @@ -75,147 +78,24 @@ impl<'a> System<'a> for Sys { } } - // rtsim.tick += 1; - - // Update unloaded rtsim entities, in groups at a time - /* - const TICK_STAGGER: usize = 30; - let entities_per_iteration = rtsim.entities.len() / TICK_STAGGER; - let mut to_reify = Vec::new(); - for (id, entity) in rtsim - .entities - .iter_mut() - .skip((rtsim.tick as usize % TICK_STAGGER) * entities_per_iteration) - .take(entities_per_iteration) - .filter(|(_, e)| !e.is_loaded) - { - if rtsim - .chunk_states - .get(entity.pos.xy()) - .copied() - .unwrap_or(false) - { - to_reify.push(id); - } else { - // Simulate behaviour - if let Some(travel_to) = &entity.controller.travel_to { - // Move towards target at approximate character speed - entity.pos += Vec3::from( - (travel_to.0.xy() - entity.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero) - * entity.get_body().max_speed_approx() - * entity.controller.speed_factor, - ) * dt; - } - - if let Some(alt) = world - .sim() - .get_alt_approx(entity.pos.xy().map(|e| e.floor() as i32)) - { - entity.pos.z = alt; - } - } - // entity.tick(&time, &terrain, &world, &index.as_index_ref()); - } - */ - - // Tick entity AI each time if it's loaded - // for (_, entity) in rtsim.entities.iter_mut().filter(|(_, e)| - // e.is_loaded) { entity.last_time_ticked = time.0; - // entity.tick(&time, &terrain, &world, &index.as_index_ref()); - // } - - /* - let mut server_emitter = server_event_bus.emitter(); - for id in to_reify { - rtsim.reify_entity(id); - let entity = &rtsim.entities[id]; - let rtsim_entity = Some(RtSimEntity(id)); - - let body = entity.get_body(); - let spawn_pos = terrain - .find_space(entity.pos.map(|e| e.floor() as i32)) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, body.flying_height()); - - let pos = comp::Pos(spawn_pos); - - let event = if let comp::Body::Ship(ship) = body { - ServerEvent::CreateShip { - pos, - ship, - mountable: false, - agent: Some(comp::Agent::from_body(&body)), - rtsim_entity, - } - } else { - let entity_config_path = entity.get_entity_config(); - let mut loadout_rng = entity.loadout_rng(); - let ad_hoc_loadout = entity.get_adhoc_loadout(); - // Body is rewritten so that body parameters - // are consistent between reifications - let entity_config = EntityConfig::from_asset_expect_owned(entity_config_path) - .with_body(BodyBuilder::Exact(body)); - - let mut entity_info = EntityInfo::at(pos.0) - .with_entity_config(entity_config, Some(entity_config_path), &mut loadout_rng) - .with_lazy_loadout(ad_hoc_loadout); - // Merchants can be traded with - if let Some(economy) = entity.get_trade_info(&world, &index) { - entity_info = entity_info - .with_agent_mark(comp::agent::Mark::Merchant) - .with_economy(&economy); - } - match NpcData::from_entity_info(entity_info) { - NpcData::Data { - pos, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - loot, - } => ServerEvent::CreateNpc { - pos, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - anchor: None, - loot, - rtsim_entity, - projectile: None, - }, - // EntityConfig can't represent Waypoints at all - // as of now, and if someone will try to spawn - // rtsim waypoint it is definitely error. - NpcData::Waypoint(_) => unimplemented!(), - } - }; - server_emitter.emit(event); - } - - // Update rtsim with real entity data - for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, &mut agents).join() { + // Synchronise rtsim NPC with entity data + for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, (&mut agents).maybe()).join() { rtsim - .entities + .state + .data_mut() + .npcs .get_mut(rtsim_entity.0) - .filter(|e| e.is_loaded) - .map(|entity| { - entity.pos = pos.0; - agent.rtsim_controller = entity.controller.clone(); + .filter(|npc| matches!(npc.mode, NpcMode::Loaded)) + .map(|npc| { + // Update rtsim NPC state + npc.wpos = pos.0; + + // Update entity state + if let Some(agent) = agent { + agent.rtsim_controller.travel_to = npc.target.map(|(wpos, _)| wpos); + agent.rtsim_controller.speed_factor = npc.target.map_or(1.0, |(_, sf)| sf); + } }); } - */ } } diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index d32581f147..adbe5b4bc6 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -206,6 +206,7 @@ impl<'a> System<'a> for Sys { msm: &read_data.msm, poise: read_data.poises.get(entity), stance: read_data.stances.get(entity), + rtsim_entity: read_data.rtsim_entities.get(entity), }; /////////////////////////////////////////////////////////// diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index eda52faedf..261f94034e 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -1,4 +1,4 @@ -use crate::rtsim::Entity as RtSimEntity; +use common::rtsim::RtSimEntity; use common::{ comp::{ agent::{ @@ -40,8 +40,6 @@ mod interaction; pub struct BehaviorData<'a, 'b, 'c> { pub agent: &'a mut Agent, pub agent_data: AgentData<'a>, - // TODO: Move rtsim back into AgentData after rtsim2 when it has a separate crate - // pub rtsim_entity: Option<&'a RtSimEntity>, pub read_data: &'a ReadData<'a>, pub event_emitter: &'a mut Emitter<'c, ServerEvent>, pub controller: &'a mut Controller, @@ -643,7 +641,6 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { let BehaviorData { agent, agent_data, - // rtsim_entity, read_data, event_emitter, controller, @@ -750,7 +747,7 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { controller, read_data, event_emitter, - will_ambush(/* *rtsim_entity */None, agent_data), + will_ambush(agent_data.rtsim_entity, agent_data), ); } @@ -768,9 +765,9 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { read_data, event_emitter, rng, - remembers_fight_with(/* *rtsim_entity */None, read_data, target), + remembers_fight_with(agent_data.rtsim_entity, read_data, target), ); - remember_fight(/* *rtsim_entity */ None, read_data, agent, target); + remember_fight(agent_data.rtsim_entity, read_data, agent, target); } } } @@ -780,10 +777,11 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { fn will_ambush(rtsim_entity: Option<&RtSimEntity>, agent_data: &AgentData) -> bool { // TODO: implement for rtsim2 - agent_data - .health - .map_or(false, |h| h.current() / h.maximum() > 0.7) - && rtsim_entity.map_or(false, |re| re.brain.personality.will_ambush) + // agent_data + // .health + // .map_or(false, |h| h.current() / h.maximum() > 0.7) + // && rtsim_entity.map_or(false, |re| re.brain.personality.will_ambush) + false } fn remembers_fight_with( @@ -794,11 +792,12 @@ fn remembers_fight_with( // TODO: implement for rtsim2 let name = || read_data.stats.get(other).map(|stats| stats.name.clone()); - rtsim_entity.map_or(false, |rtsim_entity| { - name().map_or(false, |name| { - rtsim_entity.brain.remembers_fight_with_character(&name) - }) - }) + // rtsim_entity.map_or(false, |rtsim_entity| { + // name().map_or(false, |name| { + // rtsim_entity.brain.remembers_fight_with_character(&name) + // }) + // }) + false } /// Remember target. diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 3d37cc6d83..29d2175f7e 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -16,7 +16,6 @@ use rand::{thread_rng, Rng}; use specs::saveload::Marker; use crate::{ - rtsim::entity::{PersonalityTrait, RtSimEntityKind}, sys::agent::util::get_entity_by_id, };