From 5614eaa7a5d8dfaa5273f51358304a17e59be7ab Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Wed, 5 Apr 2023 13:57:41 +0100 Subject: [PATCH] Cleaned up rtsim rules --- common/src/cmd.rs | 10 +- rtsim/src/lib.rs | 3 +- rtsim/src/rule.rs | 3 +- rtsim/src/rule/{setup.rs => migrate.rs} | 4 +- rtsim/src/rule/npc_ai.rs | 38 +- rtsim/src/rule/simulate_npcs.rs | 530 +++++++++++------------- rtsim/src/rule/sync_npcs.rs | 112 +++++ server/src/rtsim/tick.rs | 39 +- server/src/sys/agent/behavior_tree.rs | 11 +- 9 files changed, 397 insertions(+), 353 deletions(-) rename rtsim/src/rule/{setup.rs => migrate.rs} (98%) create mode 100644 rtsim/src/rule/sync_npcs.rs diff --git a/common/src/cmd.rs b/common/src/cmd.rs index ef152d7334..644eaf3e4e 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -296,6 +296,11 @@ pub enum ServerChatCommand { Respawn, RevokeBuild, RevokeBuildAll, + RtsimChunk, + RtsimInfo, + RtsimNpc, + RtsimPurge, + RtsimTp, Safezone, Say, Scale, @@ -310,11 +315,6 @@ pub enum ServerChatCommand { Tell, Time, Tp, - RtsimChunk, - RtsimInfo, - RtsimNpc, - RtsimPurge, - RtsimTp, Unban, Version, Waypoint, diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 3643dc458e..badf1eebd3 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -60,8 +60,9 @@ impl RtState { fn start_default_rules(&mut self) { info!("Starting default rtsim rules..."); - self.start_rule::(); + self.start_rule::(); self.start_rule::(); + self.start_rule::(); self.start_rule::(); self.start_rule::(); } diff --git a/rtsim/src/rule.rs b/rtsim/src/rule.rs index a58dd20493..00502d0e10 100644 --- a/rtsim/src/rule.rs +++ b/rtsim/src/rule.rs @@ -1,7 +1,8 @@ +pub mod migrate; pub mod npc_ai; pub mod replenish_resources; -pub mod setup; pub mod simulate_npcs; +pub mod sync_npcs; use super::RtState; use std::fmt; diff --git a/rtsim/src/rule/setup.rs b/rtsim/src/rule/migrate.rs similarity index 98% rename from rtsim/src/rule/setup.rs rename to rtsim/src/rule/migrate.rs index 55ae2cb507..047410e374 100644 --- a/rtsim/src/rule/setup.rs +++ b/rtsim/src/rule/migrate.rs @@ -4,9 +4,9 @@ use tracing::warn; /// 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 /// sensible. -pub struct Setup; +pub struct Migrate; -impl Rule for Setup { +impl Rule for Migrate { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { let data = &mut *ctx.state.data_mut(); diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 380423c5ff..ac15e8b0b2 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -20,7 +20,7 @@ use common::{ vol::RectVolSize, }; use fxhash::FxHasher64; -use itertools::Itertools; +use itertools::{Either, Itertools}; use rand::prelude::*; use rand_chacha::ChaChaRng; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; @@ -325,7 +325,7 @@ where } if let Some(path) = path_site(wpos, site_exit, site, ctx.index) { - Some(itertools::Either::Left( + Some(Either::Left( seq(path.into_iter().map(|wpos| goto_2d(wpos, 1.0, 8.0))).then(goto_2d( site_exit, speed_factor, @@ -333,14 +333,10 @@ where )), )) } else { - Some(itertools::Either::Right(goto_2d( - site_exit, - speed_factor, - 8.0, - ))) + Some(Either::Right(goto_2d(site_exit, speed_factor, 8.0))) } } else { - Some(itertools::Either::Right(goto_2d(wpos, speed_factor, 8.0))) + Some(Either::Right(goto_2d(wpos, speed_factor, 8.0))) } }) } @@ -410,9 +406,9 @@ fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action { // let track_len = ctx.world.civs().tracks.get(track_id).path().len(); // // Tracks can be traversed backward (i.e: from end to beginning). Account for this. // seq(if reversed { - // itertools::Either::Left((0..track_len).rev()) + // Either::Left((0..track_len).rev()) // } else { - // itertools::Either::Right(0..track_len) + // Either::Right(0..track_len) // } // .enumerate() // .map(move |(i, node_idx)| now(move |ctx| { @@ -456,7 +452,7 @@ 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.0003) && let Some(other) = ctx + if ctx.rng.gen_bool(0.004) && let Some(other) = ctx .state .data() .npcs @@ -680,20 +676,18 @@ fn villager(visiting_site: SiteId) -> impl Action { }) { // Walk to the plaza... - travel_to_point(plaza_wpos, 0.5) - .debug(|| "walk to plaza") - // ...then wait for some time before moving on - .then({ - let wait_time = ctx.rng.gen_range(30.0..90.0); - socialize().repeat().stop_if(timeout(wait_time)) - .debug(|| "wait at plaza") - }) - .map(|_| ()) - .boxed() + Either::Left(travel_to_point(plaza_wpos, 0.5) + .debug(|| "walk to plaza")) } else { // No plazas? :( - finish().boxed() + Either::Right(finish()) } + // ...then socialize for some time before moving on + .then(socialize() + .repeat() + .stop_if(timeout(ctx.rng.gen_range(30.0..90.0))) + .debug(|| "wait at plaza")) + .map(|_| ()) })) }) .debug(move || format!("villager at site {:?}", visiting_site)) diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 81140b6a8a..6c2794507b 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -1,16 +1,16 @@ use crate::{ data::{npc::SimulationMode, Npc}, - event::{OnDeath, OnSetup, OnTick}, + event::{EventCtx, OnDeath, OnSetup, OnTick}, RtState, Rule, RuleError, }; use common::{ comp::{self, Body}, - grid::Grid, rtsim::{Actor, NpcAction, NpcActivity, Personality}, terrain::TerrainChunkSize, vol::RectVolSize, }; -use rand::{rngs::ThreadRng, seq::SliceRandom, Rng}; +use rand::prelude::*; +use rand_chacha::ChaChaRng; use tracing::warn; use world::site::SiteKind; @@ -18,295 +18,243 @@ pub struct SimulateNpcs; impl Rule for SimulateNpcs { fn start(rtstate: &mut RtState) -> Result { - rtstate.bind::(|ctx| { - let data = &mut *ctx.state.data_mut(); - data.npcs.npc_grid = Grid::new(ctx.world.sim().get_size().as_(), Default::default()); - - for (npc_id, npc) in data.npcs.npcs.iter() { - if let Some(ride) = &npc.riding { - if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) { - let actor = Actor::Npc(npc_id); - vehicle.riders.push(actor); - if ride.steering && vehicle.driver.replace(actor).is_some() { - panic!("Replaced driver"); - } - } - } - - if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) { - home.population.insert(npc_id); - } - } - }); - - rtstate.bind::(|ctx| { - let data = &mut *ctx.state.data_mut(); - let npc_id = ctx.event.npc_id; - let Some(npc) = data.npcs.get(npc_id) else { - return; - }; - if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) { - home.population.remove(&npc_id); - } - let mut rng = rand::thread_rng(); - match npc.body { - Body::Humanoid(_) => { - if let Some((site_id, site)) = data - .sites - .iter() - .filter(|(id, site)| { - Some(*id) != npc.home - && site.faction == npc.faction - && site.world_site.map_or(false, |s| { - matches!(ctx.index.sites.get(s).kind, SiteKind::Refactor(_)) - }) - }) - .min_by_key(|(_, site)| site.population.len()) - { - let rand_wpos = |rng: &mut ThreadRng| { - let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); - wpos2d - .map(|e| e as f32 + 0.5) - .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) - }; - let random_humanoid = |rng: &mut ThreadRng| { - let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap(); - Body::Humanoid(comp::humanoid::Body::random_with(rng, species)) - }; - 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()), - ); - } else { - warn!("No site found for respawning humaniod"); - } - }, - Body::BirdLarge(_) => { - if let Some((site_id, site)) = data - .sites - .iter() - .filter(|(id, site)| { - Some(*id) != npc.home - && site.world_site.map_or(false, |s| { - matches!(ctx.index.sites.get(s).kind, SiteKind::Dungeon(_)) - }) - }) - .min_by_key(|(_, site)| site.population.len()) - { - let rand_wpos = |rng: &mut ThreadRng| { - let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); - wpos2d - .map(|e| e as f32 + 0.5) - .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) - }; - let species = [ - comp::body::bird_large::Species::Phoenix, - comp::body::bird_large::Species::Cockatrice, - comp::body::bird_large::Species::Roc, - ] - .choose(&mut rng) - .unwrap(); - data.npcs.create_npc( - Npc::new( - rng.gen(), - rand_wpos(&mut rng), - Body::BirdLarge(comp::body::bird_large::Body::random_with( - &mut rng, species, - )), - ) - .with_home(site_id), - ); - } else { - warn!("No site found for respawning bird"); - } - }, - _ => unimplemented!(), - } - }); - - rtstate.bind::(|ctx| { - let data = &mut *ctx.state.data_mut(); - for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { - let chunk_pos = - vehicle.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); - if vehicle.chunk_pos != Some(chunk_pos) { - if let Some(cell) = vehicle - .chunk_pos - .and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos)) - { - if let Some(index) = cell.vehicles.iter().position(|id| *id == vehicle_id) { - cell.vehicles.swap_remove(index); - } - } - vehicle.chunk_pos = Some(chunk_pos); - if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) { - cell.vehicles.push(vehicle_id); - } - } - } - for (npc_id, npc) in data.npcs.npcs.iter_mut() { - // Update the NPC's current site, if any - npc.current_site = ctx - .world - .sim() - .get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()) - .and_then(|chunk| { - chunk - .sites - .iter() - .find_map(|site| data.sites.world_site_map.get(site).copied()) - }); - - let chunk_pos = - npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); - if npc.chunk_pos != Some(chunk_pos) { - if let Some(cell) = npc - .chunk_pos - .and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos)) - { - if let Some(index) = cell.npcs.iter().position(|id| *id == npc_id) { - cell.npcs.swap_remove(index); - } - } - npc.chunk_pos = Some(chunk_pos); - if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) { - cell.npcs.push(npc_id); - } - } - - // Simulate the NPC's movement and interactions - if matches!(npc.mode, SimulationMode::Simulated) { - // Simulate NPC movement when riding - if let Some(riding) = &npc.riding { - if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) { - match npc.controller.activity { - // If steering, the NPC controls the vehicle's motion - Some(NpcActivity::Goto(target, speed_factor)) - if riding.steering => - { - let diff = target.xy() - vehicle.wpos.xy(); - let dist2 = diff.magnitude_squared(); - - if dist2 > 0.5f32.powi(2) { - let mut wpos = vehicle.wpos - + (diff - * (vehicle.get_speed() - * speed_factor - * ctx.event.dt - / dist2.sqrt()) - .min(1.0)) - .with_z(0.0); - - let is_valid = match vehicle.body { - common::comp::ship::Body::DefaultAirship - | common::comp::ship::Body::AirBalloon => true, - common::comp::ship::Body::SailBoat - | common::comp::ship::Body::Galleon => { - let chunk_pos = wpos.xy().as_::() - / TerrainChunkSize::RECT_SIZE.as_::(); - ctx.world - .sim() - .get(chunk_pos) - .map_or(true, |f| f.river.river_kind.is_some()) - }, - _ => false, - }; - - if is_valid { - match vehicle.body { - common::comp::ship::Body::DefaultAirship - | common::comp::ship::Body::AirBalloon => { - if let Some(alt) = ctx - .world - .sim() - .get_alt_approx(wpos.xy().as_()) - .filter(|alt| wpos.z < *alt) - { - wpos.z = alt; - } - }, - common::comp::ship::Body::SailBoat - | common::comp::ship::Body::Galleon => { - wpos.z = ctx - .world - .sim() - .get_interpolated( - wpos.xy().map(|e| e as i32), - |chunk| chunk.water_alt, - ) - .unwrap_or(0.0); - }, - _ => {}, - } - vehicle.wpos = wpos; - } - } - }, - // When riding, other actions are disabled - Some( - NpcActivity::Goto(_, _) - | NpcActivity::Gather(_) - | NpcActivity::HuntAnimals - | NpcActivity::Dance, - ) => {}, - None => {}, - } - npc.wpos = vehicle.wpos; - } else { - // Vehicle doens't exist anymore - npc.riding = None; - } - // If not riding, we assume they're just walking - } else { - match npc.controller.activity { - // Move NPCs if they have a target destination - Some(NpcActivity::Goto(target, speed_factor)) => { - let diff = target.xy() - npc.wpos.xy(); - let dist2 = diff.magnitude_squared(); - - if dist2 > 0.5f32.powi(2) { - npc.wpos += (diff - * (npc.body.max_speed_approx() - * speed_factor - * ctx.event.dt - / dist2.sqrt()) - .min(1.0)) - .with_z(0.0); - } - }, - Some( - NpcActivity::Gather(_) - | NpcActivity::HuntAnimals - | NpcActivity::Dance, - ) => { - // TODO: Maybe they should walk around randomly - // when gathering resources? - }, - None => {}, - } - } - - // Consume NPC actions - for action in std::mem::take(&mut npc.controller.actions) { - match action { - NpcAction::Greet(_) | NpcAction::Say(_) => {}, // Currently, just swallow interactions - } - } - - // Make sure NPCs remain on the surface - npc.wpos.z = ctx - .world - .sim() - .get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32)) - .unwrap_or(0.0) - + npc.body.flying_height(); - } - } - }); + rtstate.bind::(on_setup); + rtstate.bind::(on_death); + rtstate.bind::(on_tick); Ok(Self) } } + +fn on_setup(ctx: EventCtx) { + let data = &mut *ctx.state.data_mut(); + + // Add riders to vehicles + for (npc_id, npc) in data.npcs.npcs.iter() { + if let Some(ride) = &npc.riding { + if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) { + let actor = Actor::Npc(npc_id); + vehicle.riders.push(actor); + if ride.steering && vehicle.driver.replace(actor).is_some() { + panic!("Replaced driver"); + } + } + } + } +} + +fn on_death(ctx: EventCtx) { + let data = &mut *ctx.state.data_mut(); + let npc_id = ctx.event.npc_id; + let Some(npc) = data.npcs.get(npc_id) else { + return; + }; + let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()); + + // Respawn dead NPCs + match npc.body { + Body::Humanoid(_) => { + if let Some((site_id, site)) = data + .sites + .iter() + .filter(|(id, site)| { + Some(*id) != npc.home + && site.faction == npc.faction + && site.world_site.map_or(false, |s| { + matches!(ctx.index.sites.get(s).kind, SiteKind::Refactor(_)) + }) + }) + .min_by_key(|(_, site)| site.population.len()) + { + let rand_wpos = |rng: &mut ChaChaRng| { + let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); + wpos2d + .map(|e| e as f32 + 0.5) + .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) + }; + let random_humanoid = |rng: &mut ChaChaRng| { + let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap(); + Body::Humanoid(comp::humanoid::Body::random_with(rng, species)) + }; + 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()), + ); + } else { + warn!("No site found for respawning humaniod"); + } + }, + Body::BirdLarge(_) => { + if let Some((site_id, site)) = data + .sites + .iter() + .filter(|(id, site)| { + Some(*id) != npc.home + && site.world_site.map_or(false, |s| { + matches!(ctx.index.sites.get(s).kind, SiteKind::Dungeon(_)) + }) + }) + .min_by_key(|(_, site)| site.population.len()) + { + let rand_wpos = |rng: &mut ChaChaRng| { + let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); + wpos2d + .map(|e| e as f32 + 0.5) + .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) + }; + let species = [ + comp::body::bird_large::Species::Phoenix, + comp::body::bird_large::Species::Cockatrice, + comp::body::bird_large::Species::Roc, + ] + .choose(&mut rng) + .unwrap(); + data.npcs.create_npc( + Npc::new( + rng.gen(), + rand_wpos(&mut rng), + Body::BirdLarge(comp::body::bird_large::Body::random_with( + &mut rng, species, + )), + ) + .with_home(site_id), + ); + } else { + warn!("No site found for respawning bird"); + } + }, + _ => unimplemented!(), + } +} + +fn on_tick(ctx: EventCtx) { + let data = &mut *ctx.state.data_mut(); + for npc in data + .npcs + .npcs + .values_mut() + .filter(|npc| matches!(npc.mode, SimulationMode::Simulated)) + { + // Simulate NPC movement when riding + if let Some(riding) = &npc.riding { + if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) { + match npc.controller.activity { + // If steering, the NPC controls the vehicle's motion + Some(NpcActivity::Goto(target, speed_factor)) if riding.steering => { + let diff = target.xy() - vehicle.wpos.xy(); + let dist2 = diff.magnitude_squared(); + + if dist2 > 0.5f32.powi(2) { + let mut wpos = vehicle.wpos + + (diff + * (vehicle.get_speed() * speed_factor * ctx.event.dt + / dist2.sqrt()) + .min(1.0)) + .with_z(0.0); + + let is_valid = match vehicle.body { + common::comp::ship::Body::DefaultAirship + | common::comp::ship::Body::AirBalloon => true, + common::comp::ship::Body::SailBoat + | common::comp::ship::Body::Galleon => { + let chunk_pos = + wpos.xy().as_::().map2( + TerrainChunkSize::RECT_SIZE.as_::(), + |e, sz| e.div_euclid(sz), + ); + ctx.world + .sim() + .get(chunk_pos) + .map_or(true, |f| f.river.river_kind.is_some()) + }, + _ => false, + }; + + if is_valid { + match vehicle.body { + common::comp::ship::Body::DefaultAirship + | common::comp::ship::Body::AirBalloon => { + if let Some(alt) = ctx + .world + .sim() + .get_alt_approx(wpos.xy().as_()) + .filter(|alt| wpos.z < *alt) + { + wpos.z = alt; + } + }, + common::comp::ship::Body::SailBoat + | common::comp::ship::Body::Galleon => { + wpos.z = ctx + .world + .sim() + .get_interpolated( + wpos.xy().map(|e| e as i32), + |chunk| chunk.water_alt, + ) + .unwrap_or(0.0); + }, + _ => {}, + } + vehicle.wpos = wpos; + } + } + }, + // When riding, other actions are disabled + Some( + NpcActivity::Goto(_, _) + | NpcActivity::Gather(_) + | NpcActivity::HuntAnimals + | NpcActivity::Dance, + ) => {}, + None => {}, + } + npc.wpos = vehicle.wpos; + } else { + // Vehicle doens't exist anymore + npc.riding = None; + } + // If not riding, we assume they're just walking + } else { + match npc.controller.activity { + // Move NPCs if they have a target destination + Some(NpcActivity::Goto(target, speed_factor)) => { + let diff = target.xy() - npc.wpos.xy(); + let dist2 = diff.magnitude_squared(); + + if dist2 > 0.5f32.powi(2) { + npc.wpos += (diff + * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt + / dist2.sqrt()) + .min(1.0)) + .with_z(0.0); + } + }, + Some(NpcActivity::Gather(_) | NpcActivity::HuntAnimals | NpcActivity::Dance) => { + // TODO: Maybe they should walk around randomly + // when gathering resources? + }, + None => {}, + } + } + + // Consume NPC actions + for action in std::mem::take(&mut npc.controller.actions) { + match action { + NpcAction::Greet(_) | NpcAction::Say(_) => {}, /* Currently, just swallow + * interactions */ + } + } + + // Make sure NPCs remain on the surface + npc.wpos.z = ctx + .world + .sim() + .get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32)) + .unwrap_or(0.0) + + npc.body.flying_height(); + } +} diff --git a/rtsim/src/rule/sync_npcs.rs b/rtsim/src/rule/sync_npcs.rs new file mode 100644 index 0000000000..981b1c072c --- /dev/null +++ b/rtsim/src/rule/sync_npcs.rs @@ -0,0 +1,112 @@ +use crate::{ + event::{EventCtx, OnDeath, OnSetup, OnTick}, + RtState, Rule, RuleError, +}; +use common::{grid::Grid, terrain::TerrainChunkSize, vol::RectVolSize}; + +pub struct SyncNpcs; + +impl Rule for SyncNpcs { + fn start(rtstate: &mut RtState) -> Result { + rtstate.bind::(on_setup); + rtstate.bind::(on_death); + rtstate.bind::(on_tick); + + Ok(Self) + } +} + +fn on_setup(ctx: EventCtx) { + let data = &mut *ctx.state.data_mut(); + + // 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?) + 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); + } + } +} + +fn on_death(ctx: EventCtx) { + let data = &mut *ctx.state.data_mut(); + + println!("NPC DIED!"); + + // Remove NPC from home population + if let Some(home) = data + .npcs + .get(ctx.event.npc_id) + .and_then(|npc| npc.home) + .and_then(|home| data.sites.get_mut(home)) + { + home.population.remove(&ctx.event.npc_id); + } +} + +fn on_tick(ctx: EventCtx) { + let data = &mut *ctx.state.data_mut(); + // Update vehicle grid cells + for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { + let chunk_pos = vehicle.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); + if vehicle.chunk_pos != Some(chunk_pos) { + if let Some(cell) = vehicle + .chunk_pos + .and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos)) + { + if let Some(index) = cell.vehicles.iter().position(|id| *id == vehicle_id) { + cell.vehicles.swap_remove(index); + } + } + vehicle.chunk_pos = Some(chunk_pos); + if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) { + cell.vehicles.push(vehicle_id); + } + } + } + for (npc_id, npc) in data.npcs.npcs.iter_mut() { + // Update the NPC's current site, if any + npc.current_site = ctx + .world + .sim() + .get( + npc.wpos + .xy() + .as_::() + .map2(TerrainChunkSize::RECT_SIZE.as_::(), |e, sz| { + e.div_euclid(sz) + }), + ) + .and_then(|chunk| { + chunk + .sites + .iter() + .find_map(|site| data.sites.world_site_map.get(site).copied()) + }); + + // Update the NPC's grid cell + let chunk_pos = npc + .wpos + .xy() + .as_::() + .map2(TerrainChunkSize::RECT_SIZE.as_::(), |e, sz| { + e.div_euclid(sz) + }); + if npc.chunk_pos != Some(chunk_pos) { + if let Some(cell) = npc + .chunk_pos + .and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos)) + { + if let Some(index) = cell.npcs.iter().position(|id| *id == npc_id) { + cell.npcs.swap_remove(index); + } + } + npc.chunk_pos = Some(chunk_pos); + if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) { + cell.npcs.push(npc_id); + } + } + } +} diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index e39cbed114..9cec5e0fa8 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -224,30 +224,23 @@ impl<'a> System<'a> for Sys { 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? + // TODO: Other entities too like animals? 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)); + for (presence, wpos) in (&presences, &positions).join() { + if let PresenceKind::Character(character) = &presence.kind { + let chunk_pos = wpos + .0 + .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.0)); + } } } diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 96b2faaa19..5c2607b364 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -477,18 +477,18 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { NpcAction::Greet(actor) => { if bdata.agent.allowed_to_speak() && let Some(target) = bdata.read_data.lookup_actor(actor) - && let Some(target_pos) = bdata.read_data.positions.get(target) { bdata.agent.target = Some(Target::new( target, false, bdata.read_data.time.0, false, - Some(target_pos.0), + bdata.read_data.positions.get(target).map(|p| p.0), )); // We're always aware of someone we're talking to bdata.agent.awareness.set_maximally_aware(); + bdata.controller.push_action(ControlAction::Stand); bdata.controller.push_action(ControlAction::Talk); bdata.controller.push_utterance(UtteranceKind::Greeting); bdata @@ -499,20 +499,15 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { .agent .timer .start(bdata.read_data.time.0, TimerAction::Interact); - true - } else { - false } }, NpcAction::Say(msg) => { bdata.controller.push_utterance(UtteranceKind::Greeting); bdata.agent_data.chat_npc(msg, bdata.event_emitter); - false }, } - } else { - false } + false } /// Handle timed events, like looking at the player we are talking to