From 6035234c6eaa579ae768509196290759d2afa051 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 1 Apr 2023 00:20:14 +0100 Subject: [PATCH] Removed old rtsim --- server/Cargo.toml | 2 +- server/src/chunk_generator.rs | 2 +- server/src/cmd.rs | 10 +- server/src/events/entity_manipulation.rs | 4 +- server/src/lib.rs | 23 +- server/src/rtsim/chunks.rs | 34 - server/src/rtsim/entity.rs | 1074 ----------------- server/src/{rtsim2 => rtsim}/event.rs | 2 +- server/src/rtsim/load_chunks.rs | 20 - server/src/rtsim/mod.rs | 592 ++++----- server/src/{rtsim2 => rtsim}/rule.rs | 2 +- .../rule/deplete_resources.rs | 4 +- server/src/rtsim/tick.rs | 425 +++++-- server/src/rtsim/unload_chunks.rs | 43 - server/src/rtsim2/mod.rs | 242 ---- server/src/rtsim2/tick.rs | 385 ------ server/src/state_ext.rs | 2 +- server/src/sys/terrain.rs | 4 +- 18 files changed, 542 insertions(+), 2328 deletions(-) delete mode 100644 server/src/rtsim/chunks.rs delete mode 100644 server/src/rtsim/entity.rs rename server/src/{rtsim2 => rtsim}/event.rs (90%) delete mode 100644 server/src/rtsim/load_chunks.rs rename server/src/{rtsim2 => rtsim}/rule.rs (90%) rename server/src/{rtsim2 => rtsim}/rule/deplete_resources.rs (94%) delete mode 100644 server/src/rtsim/unload_chunks.rs delete mode 100644 server/src/rtsim2/mod.rs delete mode 100644 server/src/rtsim2/tick.rs diff --git a/server/Cargo.toml b/server/Cargo.toml index 38b1ea391f..54347b0473 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -23,7 +23,7 @@ common-state = { package = "veloren-common-state", path = "../common/state" } common-systems = { package = "veloren-common-systems", path = "../common/systems" } common-net = { package = "veloren-common-net", path = "../common/net" } world = { package = "veloren-world", path = "../world" } -rtsim2 = { package = "veloren-rtsim", path = "../rtsim" } +rtsim = { package = "veloren-rtsim", path = "../rtsim" } network = { package = "veloren-network", path = "../network", features = ["metrics", "compression", "quic"], default-features = false } server-agent = {package = "veloren-server-agent", path = "agent"} diff --git a/server/src/chunk_generator.rs b/server/src/chunk_generator.rs index c5ca62be7d..c76eb6b75e 100644 --- a/server/src/chunk_generator.rs +++ b/server/src/chunk_generator.rs @@ -1,6 +1,6 @@ #[cfg(not(feature = "worldgen"))] use crate::test_world::{IndexOwned, World}; -use crate::{metrics::ChunkGenMetrics, rtsim2::RtSim}; +use crate::{metrics::ChunkGenMetrics, rtsim::RtSim}; use common::{ calendar::Calendar, generation::ChunkSupplement, resources::TimeOfDay, slowjob::SlowJobPool, terrain::TerrainChunk, diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 82eb3345bf..3b3025e750 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1193,7 +1193,7 @@ fn handle_rtsim_tp( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - use crate::rtsim2::RtSim; + use crate::rtsim::RtSim; let pos = if let Some(id) = parse_cmd_args!(args, u32) { // TODO: Take some other identifier than an integer to this command. server @@ -1222,7 +1222,7 @@ fn handle_rtsim_info( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - use crate::rtsim2::RtSim; + use crate::rtsim::RtSim; if let Some(id) = parse_cmd_args!(args, u32) { // TODO: Take some other identifier than an integer to this command. let rtsim = server.state.ecs().read_resource::(); @@ -1271,7 +1271,7 @@ fn handle_rtsim_purge( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - use crate::rtsim2::RtSim; + use crate::rtsim::RtSim; if let Some(should_purge) = parse_cmd_args!(args, bool) { server .state @@ -1301,7 +1301,7 @@ fn handle_rtsim_chunk( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - use crate::rtsim2::{ChunkStates, RtSim}; + use crate::rtsim::{ChunkStates, RtSim}; let pos = position(server, target, "target")?; let chunk_key = pos.0.xy().as_::().wpos_to_cpos(); @@ -2034,7 +2034,7 @@ fn handle_kill_npcs( .get(entity) .copied() { - ecs.write_resource::() + ecs.write_resource::() .hook_rtsim_entity_delete( &ecs.read_resource::>(), ecs.read_resource::().as_index_ref(), diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index f596e94292..eff23180f2 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -7,7 +7,7 @@ use crate::{ skillset::SkillGroupKind, BuffKind, BuffSource, PhysicsState, }, - rtsim2, + rtsim, sys::terrain::SAFE_ZONE_RADIUS, Server, SpawnPoint, StateExt, }; @@ -527,7 +527,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt { state .ecs() - .write_resource::() + .write_resource::() .hook_rtsim_entity_delete( &state.ecs().read_resource::>(), state diff --git a/server/src/lib.rs b/server/src/lib.rs index 0a5ddb03b7..8946299f7e 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -30,9 +30,7 @@ pub mod metrics; pub mod persistence; mod pet; pub mod presence; -// TODO: Remove -//pub mod rtsim; -pub mod rtsim2; +pub mod rtsim; pub mod settings; pub mod state_ext; pub mod sys; @@ -566,7 +564,7 @@ impl Server { // Init rtsim, loading it from disk if possible #[cfg(feature = "worldgen")] { - match rtsim2::RtSim::new( + match rtsim::RtSim::new( &settings.world, index.as_index_ref(), &world, @@ -721,7 +719,7 @@ impl Server { // When a resource block updates, inform rtsim if old_block.get_rtsim_resource().is_some() || new_block.get_rtsim_resource().is_some() { - ecs.write_resource::().hook_block_update( + ecs.write_resource::().hook_block_update( &ecs.read_resource::>(), ecs.read_resource::().as_index_ref(), wpos, @@ -750,7 +748,7 @@ impl Server { */ #[cfg(feature = "worldgen")] { - rtsim2::add_server_systems(dispatcher_builder); + rtsim::add_server_systems(dispatcher_builder); weather::add_server_systems(dispatcher_builder); } }, @@ -871,7 +869,7 @@ impl Server { { self.state .ecs() - .write_resource::() + .write_resource::() .hook_rtsim_entity_unload(rtsim_entity); } #[cfg(feature = "worldgen")] @@ -884,7 +882,7 @@ impl Server { { self.state .ecs() - .write_resource::() + .write_resource::() .hook_rtsim_vehicle_unload(rtsim_vehicle); } @@ -1039,7 +1037,7 @@ impl Server { let client = ecs.read_storage::(); let mut terrain = ecs.write_resource::(); #[cfg(feature = "worldgen")] - let rtsim = ecs.read_resource::(); + let rtsim = ecs.read_resource::(); #[cfg(not(feature = "worldgen"))] let rtsim = (); @@ -1222,7 +1220,7 @@ impl Server { let ecs = self.state.ecs(); let slow_jobs = ecs.read_resource::(); #[cfg(feature = "worldgen")] - let rtsim = ecs.read_resource::(); + let rtsim = ecs.read_resource::(); #[cfg(not(feature = "worldgen"))] let rtsim = (); ecs.write_resource::().generate_chunk( @@ -1467,10 +1465,7 @@ impl Drop for Server { #[cfg(feature = "worldgen")] { info!("Saving rtsim state..."); - self.state - .ecs() - .write_resource::() - .save(true); + self.state.ecs().write_resource::().save(true); } } } diff --git a/server/src/rtsim/chunks.rs b/server/src/rtsim/chunks.rs deleted file mode 100644 index 481c8cab71..0000000000 --- a/server/src/rtsim/chunks.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::*; -use ::world::util::Grid; - -pub struct Chunks { - chunks: Grid, - pub chunks_to_load: Vec>, - pub chunks_to_unload: Vec>, -} - -impl Chunks { - pub fn new(size: Vec2) -> Self { - Chunks { - chunks: Grid::populate_from(size.map(|e| e as i32), |_| Chunk { is_loaded: false }), - chunks_to_load: Vec::new(), - chunks_to_unload: Vec::new(), - } - } - - pub fn chunk(&self, key: Vec2) -> Option<&Chunk> { self.chunks.get(key) } - - pub fn size(&self) -> Vec2 { self.chunks.size().map(|e| e as u32) } - - pub fn chunk_mut(&mut self, key: Vec2) -> Option<&mut Chunk> { self.chunks.get_mut(key) } - - pub fn chunk_at(&self, pos: Vec2) -> Option<&Chunk> { - self.chunks.get(pos.map2(TerrainChunk::RECT_SIZE, |e, sz| { - (e.floor() as i32).div_euclid(sz as i32) - })) - } -} - -pub struct Chunk { - pub is_loaded: bool, -} diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs deleted file mode 100644 index 57d5ecfcc0..0000000000 --- a/server/src/rtsim/entity.rs +++ /dev/null @@ -1,1074 +0,0 @@ -use super::*; -use common::{ - resources::Time, - rtsim::{Memory, MemoryItem}, - store::Id, - terrain::TerrainGrid, - trade, LoadoutBuilder, -}; -use enumset::*; -use rand_distr::{Distribution, Normal}; -use std::f32::consts::PI; -use tracing::warn; -use world::{ - civ::{Site, Track}, - util::RandomPerm, - IndexRef, World, -}; - -pub struct Entity { - pub is_loaded: bool, - pub pos: Vec3, - pub seed: u32, - pub last_time_ticked: f64, - pub controller: RtSimController, - pub kind: RtSimEntityKind, - pub brain: Brain, -} - -#[derive(Clone, Copy, strum::EnumIter, PartialEq, Eq)] -pub enum RtSimEntityKind { - Wanderer, - Cultist, - Villager, - TownGuard, - Merchant, - Blacksmith, - Chef, - Alchemist, - Prisoner, -} - -const BIRD_MEDIUM_ROSTER: &[comp::bird_medium::Species] = &[ - // Disallows flightless birds - comp::bird_medium::Species::Duck, - comp::bird_medium::Species::Goose, - comp::bird_medium::Species::Parrot, - comp::bird_medium::Species::Eagle, -]; - -const BIRD_LARGE_ROSTER: &[comp::bird_large::Species] = &[ - // Wyverns not included until proper introduction - comp::bird_large::Species::Phoenix, - comp::bird_large::Species::Cockatrice, - comp::bird_large::Species::Roc, -]; - -const PERM_SPECIES: u32 = 0; -const PERM_BODY: u32 = 1; -const PERM_LOADOUT: u32 = 2; -const PERM_LEVEL: u32 = 3; -const PERM_GENUS: u32 = 4; -const PERM_TRADE: u32 = 5; - -impl Entity { - pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed + perm) } - - pub fn loadout_rng(&self) -> impl Rng { self.rng(PERM_LOADOUT) } - - pub fn get_body(&self) -> comp::Body { - match self.kind { - RtSimEntityKind::Wanderer => { - match self.rng(PERM_GENUS).gen::() { - // we want 5% airships, 45% birds, 50% humans - x if x < 0.05 => { - comp::ship::Body::random_airship_with(&mut self.rng(PERM_BODY)).into() - }, - x if x < 0.45 => { - let species = *BIRD_MEDIUM_ROSTER - .choose(&mut self.rng(PERM_SPECIES)) - .unwrap(); - comp::bird_medium::Body::random_with(&mut self.rng(PERM_BODY), &species) - .into() - }, - x if x < 0.50 => { - let species = *BIRD_LARGE_ROSTER - .choose(&mut self.rng(PERM_SPECIES)) - .unwrap(); - comp::bird_large::Body::random_with(&mut self.rng(PERM_BODY), &species) - .into() - }, - _ => { - let species = *comp::humanoid::ALL_SPECIES - .choose(&mut self.rng(PERM_SPECIES)) - .unwrap(); - comp::humanoid::Body::random_with(&mut self.rng(PERM_BODY), &species).into() - }, - } - }, - RtSimEntityKind::Cultist - | RtSimEntityKind::Villager - | RtSimEntityKind::TownGuard - | RtSimEntityKind::Chef - | RtSimEntityKind::Alchemist - | RtSimEntityKind::Blacksmith - | RtSimEntityKind::Prisoner - | RtSimEntityKind::Merchant => { - let species = *comp::humanoid::ALL_SPECIES - .choose(&mut self.rng(PERM_SPECIES)) - .unwrap(); - comp::humanoid::Body::random_with(&mut self.rng(PERM_BODY), &species).into() - }, - } - } - - pub fn get_trade_info( - &self, - world: &World, - index: &world::IndexOwned, - ) -> Option { - let site = match self.kind { - /* - // Travelling merchants (don't work for some reason currently) - RtSimEntityKind::Wanderer if self.rng(PERM_TRADE).gen_bool(0.5) => { - match self.brain.route { - Travel::Path { target_id, .. } => Some(target_id), - _ => None, - } - }, - */ - RtSimEntityKind::Merchant => self.brain.begin_site(), - _ => None, - }?; - - let site = world.civs().sites[site].site_tmp?; - index.sites[site].trade_information(site.id()) - } - - pub fn get_entity_config(&self) -> &str { - match self.get_body() { - comp::Body::Humanoid(_) => { - let rank = match self.rng(PERM_LEVEL).gen_range::(0..=20) { - 0..=2 => TravelerRank::Rank0, - 3..=9 => TravelerRank::Rank1, - 10..=17 => TravelerRank::Rank2, - 18.. => TravelerRank::Rank3, - }; - humanoid_config(self.kind, rank) - }, - comp::Body::BirdMedium(b) => bird_medium_config(b), - comp::Body::BirdLarge(b) => bird_large_config(b), - _ => unimplemented!(), - } - } - - /// Escape hatch for runtime creation of loadout not covered by entity - /// config. - // NOTE: Signature is part of interface of EntityInfo - pub fn get_adhoc_loadout( - &self, - ) -> fn(LoadoutBuilder, Option<&trade::SiteInformation>) -> LoadoutBuilder { - let kind = self.kind; - - if let RtSimEntityKind::Merchant = kind { - |l, trade| l.with_creator(world::site::settlement::merchant_loadout, trade) - } else { - |l, _| l - } - } - - pub fn tick(&mut self, time: &Time, terrain: &TerrainGrid, world: &World, index: &IndexRef) { - self.brain.route = match self.brain.route.clone() { - Travel::Lost => { - match self.get_body() { - comp::Body::Humanoid(_) => { - if let Some(nearest_site_id) = world - .civs() - .sites - .iter() - .filter(|s| s.1.is_settlement() || s.1.is_castle()) - .min_by_key(|(_, site)| { - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32 - }) - .map(|(id, _)| id) - { - // The path choosing code works best when Humanoids can assume they are - // in a town that has at least one path. If the Human isn't in a town - // with at least one path, we need to get them to a town that does. - let nearest_site = &world.civs().sites[nearest_site_id]; - let site_wpos = - nearest_site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = - site_wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - if dist < 64_u32.pow(2) { - Travel::InSite { - site_id: nearest_site_id, - } - } else { - Travel::Direct { - target_id: nearest_site_id, - } - } - } else { - // Somehow no nearest site could be found - // Logically this should never happen, but if it does the rtsim entity - // will just sit tight - warn!("Nearest site could not be found"); - Travel::Lost - } - }, - comp::Body::Ship(_) => { - if let Some((target_id, site)) = world - .civs() - .sites - .iter() - .filter(|s| match self.get_body() { - comp::Body::Ship(_) => s.1.is_settlement(), - _ => s.1.is_dungeon(), - }) - .filter(|_| thread_rng().gen_range(0i32..4) == 0) - .min_by_key(|(_, site)| { - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = - wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - dist + if dist < 96_u32.pow(2) { 100_000_000 } else { 0 } - }) - { - let mut rng = thread_rng(); - if let (Ok(normalpos), Ok(normaloff)) = - (Normal::new(0.0, 64.0), Normal::new(0.0, 256.0)) - { - let mut path = Vec::>::default(); - let target_site_pos = - site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - (e * sz as i32 + sz as i32 / 2) as f32 - }); - let offset_site_pos = - target_site_pos.map(|v| v + normalpos.sample(&mut rng)); - let offset_dir = (offset_site_pos - self.pos.xy()).normalized(); - let dist = (offset_site_pos - self.pos.xy()).magnitude(); - let midpoint = self.pos.xy() + offset_dir * (dist / 2.0); - let perp_dir = offset_dir.rotated_z(PI / 2.0); - let offset = normaloff.sample(&mut rng); - let inbetween_pos = midpoint + (perp_dir * offset); - - path.push(inbetween_pos.map(|e| e as i32)); - path.push(target_site_pos.map(|e| e as i32)); - - Travel::CustomPath { - target_id, - path, - progress: 0, - } - } else { - Travel::Direct { target_id } - } - } else { - Travel::Lost - } - }, - _ => { - if let Some(target_id) = world - .civs() - .sites - .iter() - .filter(|s| match self.get_body() { - comp::Body::Ship(_) => s.1.is_settlement(), - _ => s.1.is_dungeon(), - }) - .filter(|_| thread_rng().gen_range(0i32..4) == 0) - .min_by_key(|(_, site)| { - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = - wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - dist + if dist < 96_u32.pow(2) { 100_000 } else { 0 } - }) - .map(|(id, _)| id) - { - Travel::Direct { target_id } - } else { - Travel::Lost - } - }, - } - }, - Travel::InSite { site_id } => { - if !self.get_body().is_humanoid() { - // Non humanoids don't care if they start at a site - Travel::Lost - } else if let Some(target_id) = world - .civs() - .neighbors(site_id) - .filter(|sid| { - let site = world.civs().sites.get(*sid); - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - dist > 96_u32.pow(2) - }) - .filter(|sid| { - if let Some(last_visited) = self.brain.last_visited { - *sid != last_visited - } else { - true - } - }) - .choose(&mut thread_rng()) - { - if let Some(track_id) = world.civs().track_between(site_id, target_id) { - self.brain.last_visited = Some(site_id); - Travel::Path { - target_id, - track_id, - progress: 0, - reversed: false, - } - } else { - // This should never trigger, since neighbors returns a list of sites for - // which a track exists going from the current town. - warn!("Could not get track after selecting from neighbor list"); - self.brain.last_visited = Some(site_id); - Travel::Direct { target_id } - } - } else if let Some(target_id) = world - .civs() - .sites - .iter() - .filter(|s| s.1.is_settlement() | s.1.is_castle()) - .filter(|_| thread_rng().gen_range(0i32..4) == 0) - .min_by_key(|(_, site)| { - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - dist + if dist < 96_u32.pow(2) { 100_000 } else { 0 } - }) - .map(|(id, _)| id) - { - // This code should only trigger when no paths out of the current town exist. - // The traveller will attempt to directly travel to another town - self.brain.last_visited = Some(site_id); - Travel::Direct { target_id } - } else { - // No paths we're picked, so stay in town. This will cause direct travel on the - // next tick. - self.brain.last_visited = Some(site_id); - Travel::InSite { site_id } - } - }, - Travel::Direct { target_id } => { - let site = &world.civs().sites[target_id]; - let destination_name = site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); - - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - - if dist < 64_u32.pow(2) { - Travel::InSite { site_id: target_id } - } else { - let travel_to = self.pos.xy() - + Vec3::from( - (wpos.map(|e| e as f32 + 0.5) - self.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero), - ) * 64.0; - let travel_to_alt = world - .sim() - .get_alt_approx(travel_to.map(|e| e as i32)) - .unwrap_or(0.0) as i32; - let travel_to = terrain - .find_space(Vec3::new( - travel_to.x as i32, - travel_to.y as i32, - travel_to_alt, - )) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, 0.0); - - self.controller.travel_to = Some((travel_to, destination_name)); - self.controller.speed_factor = 0.70; - Travel::Direct { target_id } - } - }, - Travel::CustomPath { - target_id, - path, - progress, - } => { - let site = &world.civs().sites[target_id]; - let destination_name = site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); - - if let Some(wpos) = &path.get(progress) { - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - if dist < 16_u32.pow(2) { - if progress + 1 < path.len() { - Travel::CustomPath { - target_id, - path, - progress: progress + 1, - } - } else { - Travel::InSite { site_id: target_id } - } - } else { - let travel_to = self.pos.xy() - + Vec3::from( - (wpos.map(|e| e as f32 + 0.5) - self.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero), - ) * 64.0; - let travel_to_alt = world - .sim() - .get_alt_approx(travel_to.map(|e| e as i32)) - .unwrap_or(0.0) as i32; - let travel_to = terrain - .find_space(Vec3::new( - travel_to.x as i32, - travel_to.y as i32, - travel_to_alt, - )) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, 0.0); - - self.controller.travel_to = Some((travel_to, destination_name)); - self.controller.speed_factor = 0.70; - Travel::CustomPath { - target_id, - path, - progress, - } - } - } else { - Travel::Direct { target_id } - } - }, - Travel::Path { - target_id, - track_id, - progress, - reversed, - } => { - let track = &world.civs().tracks.get(track_id); - let site = &world.civs().sites[target_id]; - let destination_name = site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); - let nth = if reversed { - track.path().len() - progress - 1 - } else { - progress - }; - - if let Some(sim_pos) = track.path().iter().nth(nth) { - let chunkpos = sim_pos.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let wpos = if let Some(pathdata) = world.sim().get_nearest_path(chunkpos) { - pathdata.1.map(|e| e as i32) - } else { - chunkpos - }; - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - - match dist { - d if d < 16_u32.pow(2) => { - if progress + 1 >= track.path().len() { - Travel::Direct { target_id } - } else { - Travel::Path { - target_id, - track_id, - progress: progress + 1, - reversed, - } - } - }, - d if d > 256_u32.pow(2) => { - if !reversed && progress == 0 { - Travel::Path { - target_id, - track_id, - progress: 0, - reversed: true, - } - } else { - Travel::Lost - } - }, - _ => { - let travel_to = self.pos.xy() - + Vec3::from( - (wpos.map(|e| e as f32 + 0.5) - self.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero), - ) * 64.0; - let travel_to_alt = world - .sim() - .get_alt_approx(travel_to.map(|e| e as i32)) - .unwrap_or(0.0) - as i32; - let travel_to = terrain - .find_space(Vec3::new( - travel_to.x as i32, - travel_to.y as i32, - travel_to_alt, - )) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, 0.0); - self.controller.travel_to = Some((travel_to, destination_name)); - self.controller.speed_factor = 0.70; - Travel::Path { - target_id, - track_id, - progress, - reversed, - } - }, - } - } else { - // This code should never trigger. If we've gone outside the bounds of the - // tracks vec then a logic bug has occured. I actually had - // an off by one error that caused this to trigger and - // resulted in travellers getting stuck in towns. - warn!("Progress out of bounds while following track"); - Travel::Lost - } - }, - Travel::DirectRaid { - target_id, - home_id, - raid_complete, - time_to_move, - } => { - // Destination site is home if raid is complete, else it is target site - let dest_site = if raid_complete { - &world.civs().sites[home_id] - } else { - &world.civs().sites[target_id] - }; - let destination_name = dest_site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); - - let wpos = dest_site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - - // Once at site, stay for a bit, then move to other site - if dist < 128_u32.pow(2) { - // If time_to_move is not set yet, use current time, ceiling to nearest multiple - // of 100, and then add another 100. - let time_to_move = if time_to_move.is_none() { - // Time increment is how long raiders stay at a site about. Is longer for - // home site and shorter for target site. - let time_increment = if raid_complete { 600.0 } else { 60.0 }; - Some((time.0 / time_increment).ceil() * time_increment + time_increment) - } else { - time_to_move - }; - - // If the time has come to move, flip raid bool - if time_to_move.map_or(false, |t| time.0 > t) { - Travel::DirectRaid { - target_id, - home_id, - raid_complete: !raid_complete, - time_to_move: None, - } - } else { - let theta = (time.0 / 30.0).floor() as f32 * self.seed as f32; - // Otherwise wander around site (or "plunder" if target site) - let travel_to = - wpos.map(|e| e as f32) + Vec2::new(theta.cos(), theta.sin()) * 100.0; - let travel_to_alt = world - .sim() - .get_alt_approx(travel_to.map(|e| e as i32)) - .unwrap_or(0.0) as i32; - let travel_to = terrain - .find_space(Vec3::new( - travel_to.x as i32, - travel_to.y as i32, - travel_to_alt, - )) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, 0.0); - - self.controller.travel_to = Some((travel_to, destination_name)); - self.controller.speed_factor = 0.75; - Travel::DirectRaid { - target_id, - home_id, - raid_complete, - time_to_move, - } - } - } else { - let travel_to = self.pos.xy() - + Vec3::from( - (wpos.map(|e| e as f32 + 0.5) - self.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero), - ) * 64.0; - let travel_to_alt = world - .sim() - .get_alt_approx(travel_to.map(|e| e as i32)) - .unwrap_or(0.0) as i32; - let travel_to = terrain - .find_space(Vec3::new( - travel_to.x as i32, - travel_to.y as i32, - travel_to_alt, - )) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, 0.0); - - self.controller.travel_to = Some((travel_to, destination_name)); - self.controller.speed_factor = 0.90; - Travel::DirectRaid { - target_id, - home_id, - raid_complete, - time_to_move, - } - } - }, - Travel::Idle => Travel::Idle, - }; - - // Forget old memories - self.brain - .memories - .retain(|memory| memory.time_to_forget > time.0); - } -} - -#[derive(Clone, Debug)] -pub enum Travel { - // The initial state all entities start in, and a fallback for when a state has stopped making - // sense. Non humanoids will always revert to this state after reaching their goal since the - // current site they are in doesn't change their behavior. - Lost, - // When an rtsim entity reaches a site it will switch to this state to restart their - // pathfinding from the beginning. Useful when the entity needs to know its current site to - // decide their next target. - InSite { - site_id: Id, - }, - // Move directly to a target site. Used by birds mostly, but also by humands who cannot find a - // path. - Direct { - target_id: Id, - }, - // Follow a custom path to reach the destination. Airships define a custom path to reduce the - // chance of collisions. - CustomPath { - target_id: Id, - path: Vec>, - progress: usize, - }, - // Follow a track defined in the track_map to reach a site. Humanoids do this whenever - // possible. - Path { - target_id: Id, - track_id: Id, - progress: usize, - reversed: bool, - }, - // Move directly towards a target site, then head back to a home territory - DirectRaid { - target_id: Id, - home_id: Id, - raid_complete: bool, - time_to_move: Option, - }, - // For testing purposes - Idle, -} - -// Based on https://en.wikipedia.org/wiki/Big_Five_personality_traits -pub struct PersonalityBase { - openness: u8, - conscientiousness: u8, - extraversion: u8, - agreeableness: u8, - neuroticism: u8, -} - -impl PersonalityBase { - /* All thresholds here are arbitrary "seems right" values. The goal is for - * most NPCs to have some kind of distinguishing trait - something - * interesting about them. We want to avoid Joe Averages. But we also - * don't want everyone to be completely weird. - */ - pub fn to_personality(&self) -> Personality { - let will_ambush = self.agreeableness < Personality::LOW_THRESHOLD - && self.conscientiousness < Personality::LOW_THRESHOLD; - let mut chat_traits: EnumSet = EnumSet::new(); - if self.openness > Personality::HIGH_THRESHOLD { - chat_traits.insert(PersonalityTrait::Open); - if self.neuroticism < Personality::MID { - chat_traits.insert(PersonalityTrait::Adventurous); - } - } else if self.openness < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Closed); - } - if self.conscientiousness > Personality::HIGH_THRESHOLD { - chat_traits.insert(PersonalityTrait::Conscientious); - if self.agreeableness < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Busybody); - } - } else if self.conscientiousness < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Unconscientious); - } - if self.extraversion > Personality::HIGH_THRESHOLD { - chat_traits.insert(PersonalityTrait::Extroverted); - } else if self.extraversion < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Introverted); - } - if self.agreeableness > Personality::HIGH_THRESHOLD { - chat_traits.insert(PersonalityTrait::Agreeable); - if self.extraversion > Personality::MID { - chat_traits.insert(PersonalityTrait::Sociable); - } - } else if self.agreeableness < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Disagreeable); - } - if self.neuroticism > Personality::HIGH_THRESHOLD { - chat_traits.insert(PersonalityTrait::Neurotic); - if self.openness > Personality::LITTLE_HIGH { - chat_traits.insert(PersonalityTrait::Seeker); - } - if self.agreeableness > Personality::LITTLE_HIGH { - chat_traits.insert(PersonalityTrait::Worried); - } - if self.extraversion < Personality::LITTLE_LOW { - chat_traits.insert(PersonalityTrait::SadLoner); - } - } else if self.neuroticism < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Stable); - } - Personality { - personality_traits: chat_traits, - will_ambush, - } - } -} - -pub struct Personality { - pub personality_traits: EnumSet, - pub will_ambush: bool, -} - -#[derive(EnumSetType)] -pub enum PersonalityTrait { - Open, - Adventurous, - Closed, - Conscientious, - Busybody, - Unconscientious, - Extroverted, - Introverted, - Agreeable, - Sociable, - Disagreeable, - Neurotic, - Seeker, - Worried, - SadLoner, - Stable, -} - -impl Personality { - pub const HIGH_THRESHOLD: u8 = Self::MAX - Self::LOW_THRESHOLD; - pub const LITTLE_HIGH: u8 = Self::MID + (Self::MAX - Self::MIN) / 20; - pub const LITTLE_LOW: u8 = Self::MID - (Self::MAX - Self::MIN) / 20; - pub const LOW_THRESHOLD: u8 = (Self::MAX - Self::MIN) / 5 * 2 + Self::MIN; - const MAX: u8 = 100; - pub const MID: u8 = (Self::MAX - Self::MIN) / 2; - const MIN: u8 = 0; - - pub fn random_chat_trait(&self, rng: &mut impl Rng) -> Option { - self.personality_traits.into_iter().choose(rng) - } - - pub fn random_trait_value_bounded(rng: &mut impl Rng, min: u8, max: u8) -> u8 { - let max_third = max / 3; - let min_third = min / 3; - rng.gen_range(min_third..=max_third) - + rng.gen_range(min_third..=max_third) - + rng.gen_range((min - 2 * min_third)..=(max - 2 * max_third)) - } - - pub fn random_trait_value(rng: &mut impl Rng) -> u8 { - Self::random_trait_value_bounded(rng, Self::MIN, Self::MAX) - } - - pub fn random(rng: &mut impl Rng) -> Personality { - let mut random_value = - || rng.gen_range(0..=33) + rng.gen_range(0..=34) + rng.gen_range(0..=33); - let base = PersonalityBase { - openness: random_value(), - conscientiousness: random_value(), - extraversion: random_value(), - agreeableness: random_value(), - neuroticism: random_value(), - }; - base.to_personality() - } -} - -pub struct Brain { - pub begin: Option>, - pub tgt: Option>, - pub route: Travel, - pub last_visited: Option>, - pub memories: Vec, - pub personality: Personality, -} - -impl Brain { - pub fn idle(rng: &mut impl Rng) -> Self { - Self { - begin: None, - tgt: None, - route: Travel::Idle, - last_visited: None, - memories: Vec::new(), - personality: Personality::random(rng), - } - } - - pub fn raid(home_id: Id, target_id: Id, rng: &mut impl Rng) -> Self { - Self { - begin: None, - tgt: None, - route: Travel::DirectRaid { - target_id, - home_id, - raid_complete: false, - time_to_move: None, - }, - last_visited: None, - memories: Vec::new(), - personality: Personality::random(rng), - } - } - - pub fn villager(home_id: Id, rng: &mut impl Rng) -> Self { - Self { - begin: Some(home_id), - tgt: None, - route: Travel::Idle, - last_visited: None, - memories: Vec::new(), - personality: Personality::random(rng), - } - } - - pub fn merchant(home_id: Id, rng: &mut impl Rng) -> Self { - // Merchants are generally extraverted and agreeable - let extraversion_bias = (Personality::MAX - Personality::MIN) / 10 * 3; - let extraversion = - Personality::random_trait_value_bounded(rng, extraversion_bias, Personality::MAX); - let agreeableness_bias = extraversion_bias / 2; - let agreeableness = - Personality::random_trait_value_bounded(rng, agreeableness_bias, Personality::MAX); - let personality_base = PersonalityBase { - openness: Personality::random_trait_value(rng), - conscientiousness: Personality::random_trait_value(rng), - extraversion, - agreeableness, - neuroticism: Personality::random_trait_value(rng), - }; - Self { - begin: Some(home_id), - tgt: None, - route: Travel::Idle, - last_visited: None, - memories: Vec::new(), - personality: personality_base.to_personality(), - } - } - - pub fn town_guard(home_id: Id, rng: &mut impl Rng) -> Self { - Self { - begin: Some(home_id), - tgt: None, - route: Travel::Idle, - last_visited: None, - memories: Vec::new(), - personality: Personality::random(rng), - } - } - - pub fn begin_site(&self) -> Option> { self.begin } - - pub fn add_memory(&mut self, memory: Memory) { self.memories.push(memory); } - - pub fn forget_enemy(&mut self, to_forget: &str) { - self.memories.retain(|memory| { - !matches!( - &memory.item, - MemoryItem::CharacterFight {name, ..} if name == to_forget) - }) - } - - pub fn remembers_mood(&self) -> bool { - self.memories - .iter() - .any(|memory| matches!(&memory.item, MemoryItem::Mood { .. })) - } - - pub fn set_mood(&mut self, memory: Memory) { - if let MemoryItem::Mood { .. } = memory.item { - if self.remembers_mood() { - while let Some(position) = self - .memories - .iter() - .position(|mem| matches!(&mem.item, MemoryItem::Mood { .. })) - { - self.memories.remove(position); - } - } - self.add_memory(memory); - }; - } - - pub fn get_mood(&self) -> Option<&Memory> { - self.memories - .iter() - .find(|memory| matches!(&memory.item, MemoryItem::Mood { .. })) - } - - pub fn remembers_character(&self, name_to_remember: &str) -> bool { - self.memories.iter().any(|memory| { - matches!( - &memory.item, - MemoryItem::CharacterInteraction { name, .. } if name == name_to_remember) - }) - } - - pub fn remembers_fight_with_character(&self, name_to_remember: &str) -> bool { - self.memories.iter().any(|memory| { - matches!( - &memory.item, - MemoryItem::CharacterFight { name, .. } if name == name_to_remember) - }) - } -} - -#[derive(strum::EnumIter)] -enum TravelerRank { - Rank0, - Rank1, - Rank2, - Rank3, -} - -fn humanoid_config(kind: RtSimEntityKind, rank: TravelerRank) -> &'static str { - match kind { - RtSimEntityKind::Cultist => "common.entity.dungeon.tier-5.cultist", - RtSimEntityKind::Wanderer => match rank { - TravelerRank::Rank0 => "common.entity.world.traveler0", - TravelerRank::Rank1 => "common.entity.world.traveler1", - TravelerRank::Rank2 => "common.entity.world.traveler2", - TravelerRank::Rank3 => "common.entity.world.traveler3", - }, - RtSimEntityKind::Villager => "common.entity.village.villager", - RtSimEntityKind::TownGuard => "common.entity.village.guard", - RtSimEntityKind::Merchant => "common.entity.village.merchant", - RtSimEntityKind::Blacksmith => "common.entity.village.blacksmith", - RtSimEntityKind::Chef => "common.entity.village.chef", - RtSimEntityKind::Alchemist => "common.entity.village.alchemist", - RtSimEntityKind::Prisoner => "common.entity.dungeon.sea_chapel.prisoner", - } -} - -fn bird_medium_config(body: comp::bird_medium::Body) -> &'static str { - match body.species { - comp::bird_medium::Species::Duck => "common.entity.wild.peaceful.duck", - comp::bird_medium::Species::Chicken => "common.entity.wild.peaceful.chicken", - comp::bird_medium::Species::Goose => "common.entity.wild.peaceful.goose", - comp::bird_medium::Species::Peacock => "common.entity.wild.peaceful.peacock", - comp::bird_medium::Species::Eagle => "common.entity.wild.peaceful.eagle", - comp::bird_medium::Species::SnowyOwl => "common.entity.wild.peaceful.snowy_owl", - comp::bird_medium::Species::HornedOwl => "common.entity.wild.peaceful.horned_owl", - comp::bird_medium::Species::Parrot => "common.entity.wild.peaceful.parrot", - _ => unimplemented!(), - } -} - -fn bird_large_config(body: comp::bird_large::Body) -> &'static str { - match body.species { - comp::bird_large::Species::Phoenix => "common.entity.wild.peaceful.phoenix", - comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice", - comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc", - // Wildcard match used here as there is an array above - // which limits what species are used - _ => unimplemented!(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use common::generation::EntityInfo; - use strum::IntoEnumIterator; - - // Brief, Incomplete and Mostly Wrong Test that all entity configs do exist. - // - // NOTE: Doesn't checks for ships, because we don't produce entity configs - // for them yet. - #[test] - fn test_entity_configs() { - let dummy_pos = Vec3::new(0.0, 0.0, 0.0); - let mut dummy_rng = thread_rng(); - // Bird Large test - for bird_large_species in BIRD_LARGE_ROSTER { - let female_body = comp::bird_large::Body { - species: *bird_large_species, - body_type: comp::bird_large::BodyType::Female, - }; - let male_body = comp::bird_large::Body { - species: *bird_large_species, - body_type: comp::bird_large::BodyType::Male, - }; - - let female_config = bird_large_config(female_body); - drop(EntityInfo::at(dummy_pos).with_asset_expect(female_config, &mut dummy_rng)); - let male_config = bird_large_config(male_body); - drop(EntityInfo::at(dummy_pos).with_asset_expect(male_config, &mut dummy_rng)); - } - // Bird Medium test - for bird_med_species in BIRD_MEDIUM_ROSTER { - let female_body = comp::bird_medium::Body { - species: *bird_med_species, - body_type: comp::bird_medium::BodyType::Female, - }; - let male_body = comp::bird_medium::Body { - species: *bird_med_species, - body_type: comp::bird_medium::BodyType::Male, - }; - - let female_config = bird_medium_config(female_body); - drop(EntityInfo::at(dummy_pos).with_asset_expect(female_config, &mut dummy_rng)); - let male_config = bird_medium_config(male_body); - drop(EntityInfo::at(dummy_pos).with_asset_expect(male_config, &mut dummy_rng)); - } - // Humanoid test - for kind in RtSimEntityKind::iter() { - for rank in TravelerRank::iter() { - let config = humanoid_config(kind, rank); - drop(EntityInfo::at(dummy_pos).with_asset_expect(config, &mut dummy_rng)); - } - } - } -} diff --git a/server/src/rtsim2/event.rs b/server/src/rtsim/event.rs similarity index 90% rename from server/src/rtsim2/event.rs rename to server/src/rtsim/event.rs index 958bf458fa..50261c98ab 100644 --- a/server/src/rtsim2/event.rs +++ b/server/src/rtsim/event.rs @@ -1,5 +1,5 @@ use common::terrain::Block; -use rtsim2::Event; +use rtsim::Event; use vek::*; #[derive(Clone)] diff --git a/server/src/rtsim/load_chunks.rs b/server/src/rtsim/load_chunks.rs deleted file mode 100644 index 301037cdae..0000000000 --- a/server/src/rtsim/load_chunks.rs +++ /dev/null @@ -1,20 +0,0 @@ -use super::*; -use common::event::{EventBus, ServerEvent}; -use common_ecs::{Job, Origin, Phase, System}; -use specs::{Read, WriteExpect}; - -#[derive(Default)] -pub struct Sys; -impl<'a> System<'a> for Sys { - type SystemData = (Read<'a, EventBus>, WriteExpect<'a, RtSim>); - - const NAME: &'static str = "rtsim::load_chunks"; - const ORIGIN: Origin = Origin::Server; - const PHASE: Phase = Phase::Create; - - fn run(_job: &mut Job, (_server_event_bus, mut rtsim): Self::SystemData) { - for _chunk in std::mem::take(&mut rtsim.chunks.chunks_to_load) { - // TODO - } - } -} diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index 21a6fc0365..6b8f50e755 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -1,414 +1,242 @@ -#![allow(dead_code)] // TODO: Remove this when rtsim is fleshed out +pub mod event; +pub mod rule; +pub mod tick; -mod chunks; -pub(crate) mod entity; -mod load_chunks; -mod tick; -mod unload_chunks; - -use crate::rtsim::entity::{Personality, Travel}; - -use self::chunks::Chunks; use common::{ - comp, - rtsim::{Memory, RtSimController, RtSimEntity, RtSimId}, - terrain::TerrainChunk, + grid::Grid, + rtsim::{ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings}, + slowjob::SlowJobPool, + terrain::{Block, TerrainChunk}, vol::RectRasterableVol, }; use common_ecs::{dispatch, System}; -use common_state::State; -use rand::prelude::*; -use slab::Slab; +use enum_map::EnumMap; +use rtsim::{ + data::{npc::SimulationMode, Data, ReadError}, + event::{OnDeath, OnSetup}, + rule::Rule, + RtState, +}; use specs::{DispatcherBuilder, WorldExt}; +use std::{ + error::Error, + fs::{self, File}, + io::{self, Write}, + path::PathBuf, + sync::Arc, + time::Instant, +}; +use tracing::{debug, error, info, warn}; use vek::*; - -pub use self::entity::{Brain, Entity, RtSimEntityKind}; +use world::{IndexRef, World}; pub struct RtSim { - tick: u64, - chunks: Chunks, - entities: Slab, + file_path: PathBuf, + last_saved: Option, + state: RtState, } impl RtSim { - pub fn new(world_chunk_size: Vec2) -> Self { - Self { - tick: 0, - chunks: Chunks::new(world_chunk_size), - entities: Slab::new(), - } + pub fn new( + settings: &WorldSettings, + index: IndexRef, + world: &World, + data_dir: PathBuf, + ) -> Result { + let file_path = Self::get_file_path(data_dir); + + info!("Looking for rtsim data at {}...", file_path.display()); + let data = 'load: { + if std::env::var("RTSIM_NOLOAD").map_or(true, |v| v != "1") { + match File::open(&file_path) { + Ok(file) => { + info!("Rtsim data found. Attempting to load..."); + match Data::from_reader(io::BufReader::new(file)) { + Ok(data) => { + info!("Rtsim data loaded."); + if data.should_purge { + warn!( + "The should_purge flag was set on the rtsim data, \ + generating afresh" + ); + } else { + break 'load data; + } + }, + Err(e) => { + error!("Rtsim data failed to load: {}", e); + let mut i = 0; + loop { + let mut backup_path = file_path.clone(); + backup_path.set_extension(if i == 0 { + format!("backup_{}", i) + } else { + "ron_backup".to_string() + }); + if !backup_path.exists() { + fs::rename(&file_path, &backup_path)?; + warn!( + "Failed rtsim data was moved to {}", + backup_path.display() + ); + info!("A fresh rtsim data will now be generated."); + break; + } + i += 1; + } + }, + } + }, + Err(e) if e.kind() == io::ErrorKind::NotFound => { + info!("No rtsim data found. Generating from world...") + }, + Err(e) => return Err(e.into()), + } + } else { + warn!( + "'RTSIM_NOLOAD' is set, skipping loading of rtsim state (old state will be \ + overwritten)." + ); + } + + let data = Data::generate(settings, &world, index); + info!("Rtsim data generated."); + data + }; + + let mut this = Self { + last_saved: None, + state: RtState::new(data).with_resource(ChunkStates(Grid::populate_from( + world.sim().get_size().as_(), + |_| None, + ))), + file_path, + }; + + rule::start_rules(&mut this.state); + + this.state.emit(OnSetup, world, index); + + Ok(this) } - pub fn hook_load_chunk(&mut self, key: Vec2) { - if let Some(chunk) = self.chunks.chunk_mut(key) { - if !chunk.is_loaded { - chunk.is_loaded = true; - self.chunks.chunks_to_load.push(key); - } + fn get_file_path(mut data_dir: PathBuf) -> PathBuf { + let mut path = std::env::var("VELOREN_RTSIM") + .map(PathBuf::from) + .unwrap_or_else(|_| { + data_dir.push("rtsim"); + data_dir + }); + path.push("data.dat"); + path + } + + 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) { + *chunk_state = Some(LoadedChunkState { max_res }); } } pub fn hook_unload_chunk(&mut self, key: Vec2) { - if let Some(chunk) = self.chunks.chunk_mut(key) { - if chunk.is_loaded { - chunk.is_loaded = false; - self.chunks.chunks_to_unload.push(key); + if let Some(chunk_state) = self.state.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, + ) { + self.state + .emit(event::OnBlockChange { wpos, old, new }, 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) { + 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) { + vehicle.mode = SimulationMode::Simulated; + } + } + + pub fn hook_rtsim_entity_delete( + &mut self, + world: &World, + index: IndexRef, + entity: RtSimEntity, + ) { + // Should entity deletion be death? They're not exactly the same thing... + self.state.emit(OnDeath { npc_id: entity.0 }, world, index); + self.state.data_mut().npcs.remove(entity.0); + } + + pub fn save(&mut self, /* slowjob_pool: &SlowJobPool, */ wait_until_finished: bool) { + info!("Saving rtsim data..."); + let file_path = self.file_path.clone(); + let data = self.state.data().clone(); + debug!("Starting rtsim data save job..."); + // TODO: Use slow job + // slowjob_pool.spawn("RTSIM_SAVE", move || { + let handle = std::thread::spawn(move || { + let tmp_file_name = "data_tmp.dat"; + if let Err(e) = file_path + .parent() + .map(|dir| { + fs::create_dir_all(dir)?; + // We write to a temporary file and then rename to avoid corruption. + Ok(dir.join(tmp_file_name)) + }) + .unwrap_or_else(|| Ok(tmp_file_name.into())) + .and_then(|tmp_file_path| Ok((File::create(&tmp_file_path)?, tmp_file_path))) + .map_err(|e: io::Error| Box::new(e) as Box) + .and_then(|(mut file, tmp_file_path)| { + debug!("Writing rtsim data to file..."); + data.write_to(io::BufWriter::new(&mut file))?; + file.flush()?; + drop(file); + fs::rename(tmp_file_path, file_path)?; + debug!("Rtsim data saved."); + Ok(()) + }) + { + error!("Saving rtsim data failed: {}", e); } + }); + + if wait_until_finished { + handle.join().expect("Save thread failed to join"); } + + self.last_saved = Some(Instant::now()); } - pub fn assimilate_entity(&mut self, entity: RtSimId) { - // tracing::info!("Assimilated rtsim entity {}", entity); - self.entities.get_mut(entity).map(|e| e.is_loaded = false); + // TODO: Clean up this API a bit + pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { + self.state.data().nature.get_chunk_resources(key) } - pub fn reify_entity(&mut self, entity: RtSimId) { - // tracing::info!("Reified rtsim entity {}", entity); - self.entities.get_mut(entity).map(|e| e.is_loaded = true); - } + pub fn state(&self) -> &RtState { &self.state } - pub fn update_entity(&mut self, entity: RtSimId, pos: Vec3) { - self.entities.get_mut(entity).map(|e| e.pos = pos); + pub fn set_should_purge(&mut self, should_purge: bool) { + self.state.data_mut().should_purge = should_purge; } +} - pub fn destroy_entity(&mut self, entity: RtSimId) { - // tracing::info!("Destroyed rtsim entity {}", entity); - self.entities.remove(entity); - } +pub struct ChunkStates(pub Grid>); - pub fn get_entity(&self, entity: RtSimId) -> Option<&Entity> { self.entities.get(entity) } - - pub fn insert_entity_memory(&mut self, entity: RtSimId, memory: Memory) { - self.entities - .get_mut(entity) - .map(|entity| entity.brain.add_memory(memory)); - } - - pub fn forget_entity_enemy(&mut self, entity: RtSimId, name: &str) { - if let Some(entity) = self.entities.get_mut(entity) { - entity.brain.forget_enemy(name); - } - } - - pub fn set_entity_mood(&mut self, entity: RtSimId, memory: Memory) { - self.entities - .get_mut(entity) - .map(|entity| entity.brain.set_mood(memory)); - } +pub struct LoadedChunkState { + // The maximum possible number of each resource in this chunk + pub max_res: EnumMap, } pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { - dispatch::(dispatch_builder, &[]); - dispatch::(dispatch_builder, &[&unload_chunks::Sys::sys_name()]); - dispatch::(dispatch_builder, &[ - &load_chunks::Sys::sys_name(), - &unload_chunks::Sys::sys_name(), - ]); -} - -pub fn init( - state: &mut State, - #[cfg(feature = "worldgen")] world: &world::World, - #[cfg(feature = "worldgen")] index: world::IndexRef, -) { - #[cfg(feature = "worldgen")] - let mut rtsim = RtSim::new(world.sim().get_size()); - #[cfg(not(feature = "worldgen"))] - let mut rtsim = RtSim::new(Vec2::new(40, 40)); - - // TODO: Determine number of rtsim entities based on things like initial site - // populations rather than world size - #[cfg(feature = "worldgen")] - { - for _ in 0..world.sim().get_size().product() / 400 { - let pos = rtsim - .chunks - .size() - .map2(TerrainChunk::RECT_SIZE, |sz, chunk_sz| { - thread_rng().gen_range(0..sz * chunk_sz) as i32 - }); - - rtsim.entities.insert(Entity { - is_loaded: false, - pos: Vec3::from(pos.map(|e| e as f32)), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Wanderer, - brain: Brain { - begin: None, - tgt: None, - route: Travel::Lost, - last_visited: None, - memories: Vec::new(), - personality: Personality::random(&mut thread_rng()), - }, - }); - } - for (site_id, site) in world - .civs() - .sites - .iter() - .filter_map(|(site_id, site)| site.site_tmp.map(|id| (site_id, &index.sites[id]))) - { - use world::site::SiteKind; - match &site.kind { - #[allow(clippy::single_match)] - SiteKind::Dungeon(dungeon) => match dungeon.dungeon_difficulty() { - Some(5) => { - let pos = site.get_origin(); - if let Some(nearest_village) = world - .civs() - .sites - .iter() - .filter(|(_, site)| site.is_settlement()) - .min_by_key(|(_, site)| { - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - wpos.map(|e| e as f32) - .distance_squared(pos.map(|x| x as f32)) - as u32 - }) - .map(|(id, _)| id) - { - for _ in 0..25 { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: Vec3::from(pos.map(|e| e as f32)), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Cultist, - brain: Brain::raid(site_id, nearest_village, &mut thread_rng()), - }); - } - } - }, - _ => {}, - }, - SiteKind::Refactor(site2) => { - // villagers - for _ in 0..site.economy.population().min(site2.plots().len() as f32) as usize { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plots() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |plot| { - site2.tile_center_wpos(plot.root_tile()) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Villager, - brain: Brain::villager(site_id, &mut thread_rng()), - }); - } - - // guards - for _ in 0..site2.plazas().len() { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plazas() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |p| { - site2.tile_center_wpos(site2.plot(p).root_tile()) - + Vec2::new( - thread_rng().gen_range(-8..9), - thread_rng().gen_range(-8..9), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::TownGuard, - brain: Brain::town_guard(site_id, &mut thread_rng()), - }); - } - - // merchants - for _ in 0..site2.plazas().len() { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plazas() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |p| { - site2.tile_center_wpos(site2.plot(p).root_tile()) - + Vec2::new( - thread_rng().gen_range(-8..9), - thread_rng().gen_range(-8..9), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Merchant, - brain: Brain::merchant(site_id, &mut thread_rng()), - }); - } - }, - SiteKind::CliffTown(site2) => { - for _ in 0..(site2.plazas().len() as f32 * 1.5) as usize { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plazas() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |p| { - site2.tile_center_wpos(site2.plot(p).root_tile()) - + Vec2::new( - thread_rng().gen_range(-8..9), - thread_rng().gen_range(-8..9), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Merchant, - brain: Brain::merchant(site_id, &mut thread_rng()), - }); - } - }, - SiteKind::SavannahPit(site2) => { - for _ in 0..4 { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plots() - .filter(|plot| { - matches!(plot.kind(), world::site2::PlotKind::SavannahPit(_)) - }) - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |plot| { - site2.tile_center_wpos( - plot.root_tile() - + Vec2::new( - thread_rng().gen_range(-5..5), - thread_rng().gen_range(-5..5), - ), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Merchant, - brain: Brain::merchant(site_id, &mut thread_rng()), - }); - } - }, - SiteKind::DesertCity(site2) => { - // villagers - for _ in 0..(site2.plazas().len() as f32 * 1.5) as usize { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plots() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |plot| { - site2.tile_center_wpos(plot.root_tile()) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Villager, - brain: Brain::villager(site_id, &mut thread_rng()), - }); - } - - // guards - for _ in 0..site2.plazas().len() { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plazas() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |p| { - site2.tile_center_wpos(site2.plot(p).root_tile()) - + Vec2::new( - thread_rng().gen_range(-8..9), - thread_rng().gen_range(-8..9), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::TownGuard, - brain: Brain::town_guard(site_id, &mut thread_rng()), - }); - } - - // merchants - for _ in 0..site2.plazas().len() { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plazas() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |p| { - site2.tile_center_wpos(site2.plot(p).root_tile()) - + Vec2::new( - thread_rng().gen_range(-8..9), - thread_rng().gen_range(-8..9), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Merchant, - brain: Brain::merchant(site_id, &mut thread_rng()), - }); - } - }, - SiteKind::ChapelSite(site2) => { - // prisoners - for _ in 0..10 { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plots() - .filter(|plot| { - matches!(plot.kind(), world::site2::PlotKind::SeaChapel(_)) - }) - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |plot| { - site2.tile_center_wpos(Vec2::new( - plot.root_tile().x, - plot.root_tile().y + 4, - )) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Prisoner, - brain: Brain::villager(site_id, &mut thread_rng()), - }); - } - }, - _ => {}, - } - } - } - - state.ecs_mut().insert(rtsim); - state.ecs_mut().register::(); - tracing::info!("Initiated real-time world simulation"); + dispatch::(dispatch_builder, &[]); } diff --git a/server/src/rtsim2/rule.rs b/server/src/rtsim/rule.rs similarity index 90% rename from server/src/rtsim2/rule.rs rename to server/src/rtsim/rule.rs index 9b73ea9165..2c499c1c60 100644 --- a/server/src/rtsim2/rule.rs +++ b/server/src/rtsim/rule.rs @@ -1,6 +1,6 @@ pub mod deplete_resources; -use rtsim2::RtState; +use rtsim::RtState; use tracing::info; pub fn start_rules(rtstate: &mut RtState) { diff --git a/server/src/rtsim2/rule/deplete_resources.rs b/server/src/rtsim/rule/deplete_resources.rs similarity index 94% rename from server/src/rtsim2/rule/deplete_resources.rs rename to server/src/rtsim/rule/deplete_resources.rs index ebea23e721..6414296012 100644 --- a/server/src/rtsim2/rule/deplete_resources.rs +++ b/server/src/rtsim/rule/deplete_resources.rs @@ -1,9 +1,9 @@ -use crate::rtsim2::{event::OnBlockChange, ChunkStates}; +use crate::rtsim::{event::OnBlockChange, ChunkStates}; use common::{ terrain::{CoordinateConversions, TerrainChunk}, vol::RectRasterableVol, }; -use rtsim2::{RtState, Rule, RuleError}; +use rtsim::{RtState, Rule, RuleError}; pub struct DepleteResources; diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 6b6571904f..63913c4267 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -3,29 +3,190 @@ use super::*; use crate::sys::terrain::NpcData; use common::{ - comp, - event::{EventBus, ServerEvent}, + comp::{self, inventory::loadout::Loadout, skillset::skills, Agent, Body}, + event::{EventBus, NpcBuilder, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, - resources::{DeltaTime, Time}, - terrain::TerrainGrid, + lottery::LootSpec, + resources::{DeltaTime, Time, TimeOfDay}, + rtsim::{RtSimController, RtSimEntity, RtSimVehicle}, + slowjob::SlowJobPool, + terrain::CoordinateConversions, + trade::{Good, SiteInformation}, + LoadoutBuilder, SkillSetBuilder, }; use common_ecs::{Job, Origin, Phase, System}; +use rtsim::data::{ + npc::{Profession, SimulationMode}, + Actor, Npc, Sites, +}; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; +use world::site::settlement::trader_loadout; + +fn humanoid_config(profession: &Profession) -> &'static str { + match profession { + Profession::Farmer => "common.entity.village.farmer", + Profession::Hunter => "common.entity.village.hunter", + Profession::Herbalist => "common.entity.village.herbalist", + Profession::Captain => "common.entity.village.captain", + Profession::Merchant => "common.entity.village.merchant", + Profession::Guard => "common.entity.village.guard", + Profession::Adventurer(rank) => match rank { + 0 => "common.entity.world.traveler0", + 1 => "common.entity.world.traveler1", + 2 => "common.entity.world.traveler2", + 3 => "common.entity.world.traveler3", + _ => panic!("Not a valid adventurer rank"), + }, + Profession::Blacksmith => "common.entity.village.blacksmith", + Profession::Chef => "common.entity.village.chef", + Profession::Alchemist => "common.entity.village.alchemist", + Profession::Pirate => "common.entity.spot.pirate", + Profession::Cultist => "common.entity.dungeon.tier-5.cultist", + } +} + +fn loadout_default(loadout: LoadoutBuilder, _economy: Option<&SiteInformation>) -> LoadoutBuilder { + loadout +} + +fn merchant_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |_| true) +} + +fn farmer_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) +} + +fn herbalist_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| { + matches!(good, Good::Ingredients) + }) +} + +fn chef_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) +} + +fn blacksmith_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| { + matches!(good, Good::Tools | Good::Armor) + }) +} + +fn alchemist_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| { + matches!(good, Good::Potions) + }) +} + +fn profession_extra_loadout( + profession: Option<&Profession>, +) -> fn(LoadoutBuilder, Option<&SiteInformation>) -> LoadoutBuilder { + match profession { + Some(Profession::Merchant) => merchant_loadout, + Some(Profession::Farmer) => farmer_loadout, + Some(Profession::Herbalist) => herbalist_loadout, + Some(Profession::Chef) => chef_loadout, + Some(Profession::Blacksmith) => blacksmith_loadout, + Some(Profession::Alchemist) => alchemist_loadout, + _ => loadout_default, + } +} + +fn profession_agent_mark(profession: Option<&Profession>) -> Option { + match profession { + Some( + Profession::Merchant + | Profession::Farmer + | Profession::Herbalist + | Profession::Chef + | Profession::Blacksmith + | Profession::Alchemist, + ) => Some(comp::agent::Mark::Merchant), + Some(Profession::Guard) => Some(comp::agent::Mark::Guard), + _ => None, + } +} + +fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo { + let pos = comp::Pos(npc.wpos); + + let mut rng = npc.rng(3); + if let Some(ref profession) = npc.profession { + let economy = npc.home.and_then(|home| { + let site = sites.get(home)?.world_site?; + index.sites.get(site).trade_information(site.id()) + }); + + let config_asset = humanoid_config(profession); + + let entity_config = EntityConfig::from_asset_expect_owned(config_asset) + .with_body(BodyBuilder::Exact(npc.body)); + EntityInfo::at(pos.0) + .with_entity_config(entity_config, Some(config_asset), &mut rng) + .with_alignment(if matches!(profession, Profession::Cultist) { + comp::Alignment::Enemy + } else { + comp::Alignment::Npc + }) + .with_economy(economy.as_ref()) + .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) + .with_agent_mark(profession_agent_mark(npc.profession.as_ref())) + } else { + let config_asset = match npc.body { + Body::BirdLarge(body) => match body.species { + comp::bird_large::Species::Phoenix => "common.entity.wild.peaceful.phoenix", + comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice", + comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc", + // Wildcard match used here as there is an array above + // which limits what species are used + _ => unimplemented!(), + }, + _ => unimplemented!(), + }; + let entity_config = EntityConfig::from_asset_expect_owned(config_asset) + .with_body(BodyBuilder::Exact(npc.body)); + + EntityInfo::at(pos.0) + .with_entity_config(entity_config, Some(config_asset), &mut rng) + .with_alignment(comp::Alignment::Wild) + } +} #[derive(Default)] pub struct Sys; impl<'a> System<'a> for Sys { type SystemData = ( - Read<'a, Time>, Read<'a, DeltaTime>, + Read<'a, Time>, + Read<'a, TimeOfDay>, Read<'a, EventBus>, WriteExpect<'a, RtSim>, - ReadExpect<'a, TerrainGrid>, ReadExpect<'a, Arc>, ReadExpect<'a, world::IndexOwned>, + ReadExpect<'a, SlowJobPool>, ReadStorage<'a, comp::Pos>, ReadStorage<'a, RtSimEntity>, + ReadStorage<'a, RtSimVehicle>, WriteStorage<'a, comp::Agent>, ); @@ -36,114 +197,118 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut Job, ( + dt, time, - _dt, + time_of_day, server_event_bus, mut rtsim, - terrain, world, index, + slow_jobs, positions, rtsim_entities, + rtsim_vehicles, mut agents, ): Self::SystemData, ) { + let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; - 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) + rtsim.state.data_mut().time_of_day = *time_of_day; + rtsim + .state + .tick(&world, index.as_index_ref(), *time_of_day, *time, dt.0); + + if rtsim + .last_saved + .map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) { - // Calculating dt ourselves because the dt provided to this fn was since the - // last frame, not since the last iteration that these entities acted - let dt = (time.0 - entity.last_time_ticked) as f32; - entity.last_time_ticked = time.0; + // TODO: Use slow jobs + let _ = slow_jobs; + rtsim.save(/* &slow_jobs, */ false); + } - if rtsim - .chunks - .chunk_at(entity.pos.xy()) - .map(|c| c.is_loaded) - .unwrap_or(false) + let chunk_states = rtsim.state.resource::(); + let data = &mut *rtsim.state.data_mut(); + + for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { + let chunk = vehicle.wpos.xy().as_::().wpos_to_cpos(); + + if matches!(vehicle.mode, SimulationMode::Simulated) + && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) { - 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; - } + vehicle.mode = SimulationMode::Loaded; - if let Some(alt) = world - .sim() - .get_alt_approx(entity.pos.xy().map(|e| e.floor() as i32)) - { - entity.pos.z = alt; - } + let mut actor_info = |actor: Actor| { + let npc_id = actor.npc()?; + let npc = data.npcs.npcs.get_mut(npc_id)?; + if matches!(npc.mode, SimulationMode::Simulated) { + npc.mode = SimulationMode::Loaded; + let entity_info = + get_npc_entity_info(npc, &data.sites, index.as_index_ref()); + + Some(match NpcData::from_entity_info(entity_info) { + NpcData::Data { + pos: _, + stats, + skill_set, + health, + poise, + inventory, + agent, + body, + alignment, + scale, + loot, + } => NpcBuilder::new(stats, body, alignment) + .with_skill_set(skill_set) + .with_health(health) + .with_poise(poise) + .with_inventory(inventory) + .with_agent(agent) + .with_scale(scale) + .with_loot(loot) + .with_rtsim(RtSimEntity(npc_id)), + // 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!(), + }) + } else { + error!("Npc is loaded but vehicle is unloaded"); + None + } + }; + + emitter.emit(ServerEvent::CreateShip { + pos: comp::Pos(vehicle.wpos), + ship: vehicle.body, + // agent: None,//Some(Agent::from_body(&Body::Ship(ship))), + rtsim_entity: Some(RtSimVehicle(vehicle_id)), + driver: vehicle.driver.and_then(&mut actor_info), + passangers: vehicle + .riders + .iter() + .copied() + .filter(|actor| vehicle.driver != Some(*actor)) + .filter_map(actor_info) + .collect(), + }); } - 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()); - } + for (npc_id, npc) in data.npcs.npcs.iter_mut() { + let chunk = npc.wpos.xy().as_::().wpos_to_cpos(); - 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)); + // Load the NPC into the world if it's in a loaded chunk and is not already + // loaded + if matches!(npc.mode, SimulationMode::Simulated) + && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) + { + npc.mode = SimulationMode::Loaded; + let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); - 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) { + emitter.emit(match NpcData::from_entity_info(entity_info) { NpcData::Data { pos, stats, @@ -158,38 +323,62 @@ impl<'a> System<'a> for Sys { loot, } => ServerEvent::CreateNpc { pos, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - anchor: None, - loot, - rtsim_entity, - projectile: None, + npc: NpcBuilder::new(stats, body, alignment) + .with_skill_set(skill_set) + .with_health(health) + .with_poise(poise) + .with_inventory(inventory) + .with_agent(agent) + .with_scale(scale) + .with_loot(loot) + .with_rtsim(RtSimEntity(npc_id)), }, // 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() { - rtsim - .entities + // Synchronise rtsim NPC with entity data + for (pos, rtsim_vehicle) in (&positions, &rtsim_vehicles).join() { + data.npcs + .vehicles + .get_mut(rtsim_vehicle.0) + .filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) + .map(|vehicle| { + // Update rtsim NPC state + vehicle.wpos = pos.0; + }); + } + + // Synchronise rtsim NPC with entity data + for (pos, rtsim_entity, agent) in + (&positions, &rtsim_entities, (&mut agents).maybe()).join() + { + data.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, SimulationMode::Loaded)) + .map(|npc| { + // Update rtsim NPC state + npc.wpos = pos.0; + + // Update entity state + if let Some(agent) = agent { + agent.rtsim_controller.personality = npc.personality; + if let Some(action) = npc.action { + match action { + rtsim::data::npc::NpcAction::Goto(wpos, sf) => { + agent.rtsim_controller.travel_to = Some(wpos); + agent.rtsim_controller.speed_factor = sf; + }, + } + } else { + agent.rtsim_controller.travel_to = None; + agent.rtsim_controller.speed_factor = 1.0; + } + } }); } } diff --git a/server/src/rtsim/unload_chunks.rs b/server/src/rtsim/unload_chunks.rs deleted file mode 100644 index 5433164dc3..0000000000 --- a/server/src/rtsim/unload_chunks.rs +++ /dev/null @@ -1,43 +0,0 @@ -use super::*; -use common::{ - comp::Pos, - event::{EventBus, ServerEvent}, - terrain::TerrainGrid, -}; -use common_ecs::{Job, Origin, Phase, System}; -use specs::{Entities, Read, ReadExpect, ReadStorage, WriteExpect}; - -#[derive(Default)] -pub struct Sys; -impl<'a> System<'a> for Sys { - type SystemData = ( - Read<'a, EventBus>, - WriteExpect<'a, RtSim>, - ReadExpect<'a, TerrainGrid>, - Entities<'a>, - ReadStorage<'a, RtSimEntity>, - ReadStorage<'a, Pos>, - ); - - const NAME: &'static str = "rtsim::unload_chunks"; - const ORIGIN: Origin = Origin::Server; - const PHASE: Phase = Phase::Create; - - fn run( - _job: &mut Job, - ( - _server_event_bus, - mut rtsim, - _terrain_grid, - _entities, - _rtsim_entities, - _positions, - ): Self::SystemData, - ) { - let chunks = std::mem::take(&mut rtsim.chunks.chunks_to_unload); - - for _chunk in chunks { - // TODO - } - } -} diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs deleted file mode 100644 index bb9ffb763b..0000000000 --- a/server/src/rtsim2/mod.rs +++ /dev/null @@ -1,242 +0,0 @@ -pub mod event; -pub mod rule; -pub mod tick; - -use common::{ - grid::Grid, - rtsim::{ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings}, - slowjob::SlowJobPool, - terrain::{Block, TerrainChunk}, - vol::RectRasterableVol, -}; -use common_ecs::{dispatch, System}; -use enum_map::EnumMap; -use rtsim2::{ - data::{npc::SimulationMode, Data, ReadError}, - event::{OnDeath, OnSetup}, - rule::Rule, - RtState, -}; -use specs::{DispatcherBuilder, WorldExt}; -use std::{ - error::Error, - fs::{self, File}, - io::{self, Write}, - path::PathBuf, - sync::Arc, - time::Instant, -}; -use tracing::{debug, error, info, warn}; -use vek::*; -use world::{IndexRef, World}; - -pub struct RtSim { - file_path: PathBuf, - last_saved: Option, - state: RtState, -} - -impl RtSim { - pub fn new( - settings: &WorldSettings, - index: IndexRef, - world: &World, - data_dir: PathBuf, - ) -> Result { - let file_path = Self::get_file_path(data_dir); - - info!("Looking for rtsim data at {}...", file_path.display()); - let data = 'load: { - if std::env::var("RTSIM_NOLOAD").map_or(true, |v| v != "1") { - match File::open(&file_path) { - Ok(file) => { - info!("Rtsim data found. Attempting to load..."); - match Data::from_reader(io::BufReader::new(file)) { - Ok(data) => { - info!("Rtsim data loaded."); - if data.should_purge { - warn!( - "The should_purge flag was set on the rtsim data, \ - generating afresh" - ); - } else { - break 'load data; - } - }, - Err(e) => { - error!("Rtsim data failed to load: {}", e); - let mut i = 0; - loop { - let mut backup_path = file_path.clone(); - backup_path.set_extension(if i == 0 { - format!("backup_{}", i) - } else { - "ron_backup".to_string() - }); - if !backup_path.exists() { - fs::rename(&file_path, &backup_path)?; - warn!( - "Failed rtsim data was moved to {}", - backup_path.display() - ); - info!("A fresh rtsim data will now be generated."); - break; - } - i += 1; - } - }, - } - }, - Err(e) if e.kind() == io::ErrorKind::NotFound => { - info!("No rtsim data found. Generating from world...") - }, - Err(e) => return Err(e.into()), - } - } else { - warn!( - "'RTSIM_NOLOAD' is set, skipping loading of rtsim state (old state will be \ - overwritten)." - ); - } - - let data = Data::generate(settings, &world, index); - info!("Rtsim data generated."); - data - }; - - let mut this = Self { - last_saved: None, - state: RtState::new(data).with_resource(ChunkStates(Grid::populate_from( - world.sim().get_size().as_(), - |_| None, - ))), - file_path, - }; - - rule::start_rules(&mut this.state); - - this.state.emit(OnSetup, world, index); - - Ok(this) - } - - fn get_file_path(mut data_dir: PathBuf) -> PathBuf { - let mut path = std::env::var("VELOREN_RTSIM") - .map(PathBuf::from) - .unwrap_or_else(|_| { - data_dir.push("rtsim"); - data_dir - }); - path.push("data.dat"); - path - } - - 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) { - *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) { - *chunk_state = None; - } - } - - pub fn hook_block_update( - &mut self, - world: &World, - index: IndexRef, - wpos: Vec3, - old: Block, - new: Block, - ) { - self.state - .emit(event::OnBlockChange { wpos, old, new }, 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) { - 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) { - vehicle.mode = SimulationMode::Simulated; - } - } - - pub fn hook_rtsim_entity_delete( - &mut self, - world: &World, - index: IndexRef, - entity: RtSimEntity, - ) { - // Should entity deletion be death? They're not exactly the same thing... - self.state.emit(OnDeath { npc_id: entity.0 }, world, index); - self.state.data_mut().npcs.remove(entity.0); - } - - pub fn save(&mut self, /* slowjob_pool: &SlowJobPool, */ wait_until_finished: bool) { - info!("Saving rtsim data..."); - let file_path = self.file_path.clone(); - let data = self.state.data().clone(); - debug!("Starting rtsim data save job..."); - // TODO: Use slow job - // slowjob_pool.spawn("RTSIM_SAVE", move || { - let handle = std::thread::spawn(move || { - let tmp_file_name = "data_tmp.dat"; - if let Err(e) = file_path - .parent() - .map(|dir| { - fs::create_dir_all(dir)?; - // We write to a temporary file and then rename to avoid corruption. - Ok(dir.join(tmp_file_name)) - }) - .unwrap_or_else(|| Ok(tmp_file_name.into())) - .and_then(|tmp_file_path| Ok((File::create(&tmp_file_path)?, tmp_file_path))) - .map_err(|e: io::Error| Box::new(e) as Box) - .and_then(|(mut file, tmp_file_path)| { - debug!("Writing rtsim data to file..."); - data.write_to(io::BufWriter::new(&mut file))?; - file.flush()?; - drop(file); - fs::rename(tmp_file_path, file_path)?; - debug!("Rtsim data saved."); - Ok(()) - }) - { - error!("Saving rtsim data failed: {}", e); - } - }); - - if wait_until_finished { - handle.join().expect("Save thread failed to join"); - } - - self.last_saved = Some(Instant::now()); - } - - // TODO: Clean up this API a bit - pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { - self.state.data().nature.get_chunk_resources(key) - } - - pub fn state(&self) -> &RtState { &self.state } - - pub fn set_should_purge(&mut self, should_purge: bool) { - self.state.data_mut().should_purge = should_purge; - } -} - -pub struct ChunkStates(pub Grid>); - -pub struct LoadedChunkState { - // The maximum possible number of each resource in this chunk - pub max_res: EnumMap, -} - -pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { - dispatch::(dispatch_builder, &[]); -} diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs deleted file mode 100644 index b6a93d7f20..0000000000 --- a/server/src/rtsim2/tick.rs +++ /dev/null @@ -1,385 +0,0 @@ -#![allow(dead_code)] // TODO: Remove this when rtsim is fleshed out - -use super::*; -use crate::sys::terrain::NpcData; -use common::{ - comp::{self, inventory::loadout::Loadout, skillset::skills, Agent, Body}, - event::{EventBus, NpcBuilder, ServerEvent}, - generation::{BodyBuilder, EntityConfig, EntityInfo}, - lottery::LootSpec, - resources::{DeltaTime, Time, TimeOfDay}, - rtsim::{RtSimController, RtSimEntity, RtSimVehicle}, - slowjob::SlowJobPool, - terrain::CoordinateConversions, - trade::{Good, SiteInformation}, - LoadoutBuilder, SkillSetBuilder, -}; -use common_ecs::{Job, Origin, Phase, System}; -use rtsim2::data::{ - npc::{Profession, SimulationMode}, - Actor, Npc, Sites, -}; -use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; -use std::{sync::Arc, time::Duration}; -use world::site::settlement::trader_loadout; - -fn humanoid_config(profession: &Profession) -> &'static str { - match profession { - Profession::Farmer => "common.entity.village.farmer", - Profession::Hunter => "common.entity.village.hunter", - Profession::Herbalist => "common.entity.village.herbalist", - Profession::Captain => "common.entity.village.captain", - Profession::Merchant => "common.entity.village.merchant", - Profession::Guard => "common.entity.village.guard", - Profession::Adventurer(rank) => match rank { - 0 => "common.entity.world.traveler0", - 1 => "common.entity.world.traveler1", - 2 => "common.entity.world.traveler2", - 3 => "common.entity.world.traveler3", - _ => panic!("Not a valid adventurer rank"), - }, - Profession::Blacksmith => "common.entity.village.blacksmith", - Profession::Chef => "common.entity.village.chef", - Profession::Alchemist => "common.entity.village.alchemist", - Profession::Pirate => "common.entity.spot.pirate", - Profession::Cultist => "common.entity.dungeon.tier-5.cultist", - } -} - -fn loadout_default(loadout: LoadoutBuilder, _economy: Option<&SiteInformation>) -> LoadoutBuilder { - loadout -} - -fn merchant_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |_| true) -} - -fn farmer_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) -} - -fn herbalist_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| { - matches!(good, Good::Ingredients) - }) -} - -fn chef_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) -} - -fn blacksmith_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| { - matches!(good, Good::Tools | Good::Armor) - }) -} - -fn alchemist_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| { - matches!(good, Good::Potions) - }) -} - -fn profession_extra_loadout( - profession: Option<&Profession>, -) -> fn(LoadoutBuilder, Option<&SiteInformation>) -> LoadoutBuilder { - match profession { - Some(Profession::Merchant) => merchant_loadout, - Some(Profession::Farmer) => farmer_loadout, - Some(Profession::Herbalist) => herbalist_loadout, - Some(Profession::Chef) => chef_loadout, - Some(Profession::Blacksmith) => blacksmith_loadout, - Some(Profession::Alchemist) => alchemist_loadout, - _ => loadout_default, - } -} - -fn profession_agent_mark(profession: Option<&Profession>) -> Option { - match profession { - Some( - Profession::Merchant - | Profession::Farmer - | Profession::Herbalist - | Profession::Chef - | Profession::Blacksmith - | Profession::Alchemist, - ) => Some(comp::agent::Mark::Merchant), - Some(Profession::Guard) => Some(comp::agent::Mark::Guard), - _ => None, - } -} - -fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo { - let pos = comp::Pos(npc.wpos); - - let mut rng = npc.rng(3); - if let Some(ref profession) = npc.profession { - let economy = npc.home.and_then(|home| { - let site = sites.get(home)?.world_site?; - index.sites.get(site).trade_information(site.id()) - }); - - let config_asset = humanoid_config(profession); - - let entity_config = EntityConfig::from_asset_expect_owned(config_asset) - .with_body(BodyBuilder::Exact(npc.body)); - EntityInfo::at(pos.0) - .with_entity_config(entity_config, Some(config_asset), &mut rng) - .with_alignment(if matches!(profession, Profession::Cultist) { - comp::Alignment::Enemy - } else { - comp::Alignment::Npc - }) - .with_economy(economy.as_ref()) - .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) - .with_agent_mark(profession_agent_mark(npc.profession.as_ref())) - } else { - let config_asset = match npc.body { - Body::BirdLarge(body) => match body.species { - comp::bird_large::Species::Phoenix => "common.entity.wild.peaceful.phoenix", - comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice", - comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc", - // Wildcard match used here as there is an array above - // which limits what species are used - _ => unimplemented!(), - }, - _ => unimplemented!(), - }; - let entity_config = EntityConfig::from_asset_expect_owned(config_asset) - .with_body(BodyBuilder::Exact(npc.body)); - - EntityInfo::at(pos.0) - .with_entity_config(entity_config, Some(config_asset), &mut rng) - .with_alignment(comp::Alignment::Wild) - } -} - -#[derive(Default)] -pub struct Sys; -impl<'a> System<'a> for Sys { - type SystemData = ( - Read<'a, DeltaTime>, - Read<'a, Time>, - Read<'a, TimeOfDay>, - Read<'a, EventBus>, - WriteExpect<'a, RtSim>, - ReadExpect<'a, Arc>, - ReadExpect<'a, world::IndexOwned>, - ReadExpect<'a, SlowJobPool>, - ReadStorage<'a, comp::Pos>, - ReadStorage<'a, RtSimEntity>, - ReadStorage<'a, RtSimVehicle>, - WriteStorage<'a, comp::Agent>, - ); - - const NAME: &'static str = "rtsim::tick"; - const ORIGIN: Origin = Origin::Server; - const PHASE: Phase = Phase::Create; - - fn run( - _job: &mut Job, - ( - dt, - time, - time_of_day, - server_event_bus, - mut rtsim, - world, - index, - slow_jobs, - positions, - rtsim_entities, - rtsim_vehicles, - mut agents, - ): Self::SystemData, - ) { - let mut emitter = server_event_bus.emitter(); - let rtsim = &mut *rtsim; - - rtsim.state.data_mut().time_of_day = *time_of_day; - rtsim - .state - .tick(&world, index.as_index_ref(), *time_of_day, *time, dt.0); - - if rtsim - .last_saved - .map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) - { - // TODO: Use slow jobs - let _ = slow_jobs; - rtsim.save(/* &slow_jobs, */ false); - } - - let chunk_states = rtsim.state.resource::(); - let data = &mut *rtsim.state.data_mut(); - - for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { - let chunk = vehicle.wpos.xy().as_::().wpos_to_cpos(); - - if matches!(vehicle.mode, SimulationMode::Simulated) - && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) - { - vehicle.mode = SimulationMode::Loaded; - - let mut actor_info = |actor: Actor| { - let npc_id = actor.npc()?; - let npc = data.npcs.npcs.get_mut(npc_id)?; - if matches!(npc.mode, SimulationMode::Simulated) { - npc.mode = SimulationMode::Loaded; - let entity_info = - get_npc_entity_info(npc, &data.sites, index.as_index_ref()); - - Some(match NpcData::from_entity_info(entity_info) { - NpcData::Data { - pos: _, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - loot, - } => NpcBuilder::new(stats, body, alignment) - .with_skill_set(skill_set) - .with_health(health) - .with_poise(poise) - .with_inventory(inventory) - .with_agent(agent) - .with_scale(scale) - .with_loot(loot) - .with_rtsim(RtSimEntity(npc_id)), - // 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!(), - }) - } else { - error!("Npc is loaded but vehicle is unloaded"); - None - } - }; - - emitter.emit(ServerEvent::CreateShip { - pos: comp::Pos(vehicle.wpos), - ship: vehicle.body, - // agent: None,//Some(Agent::from_body(&Body::Ship(ship))), - rtsim_entity: Some(RtSimVehicle(vehicle_id)), - driver: vehicle.driver.and_then(&mut actor_info), - passangers: vehicle - .riders - .iter() - .copied() - .filter(|actor| vehicle.driver != Some(*actor)) - .filter_map(actor_info) - .collect(), - }); - } - } - - for (npc_id, npc) in data.npcs.npcs.iter_mut() { - let chunk = npc.wpos.xy().as_::().wpos_to_cpos(); - - // Load the NPC into the world if it's in a loaded chunk and is not already - // loaded - if matches!(npc.mode, SimulationMode::Simulated) - && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) - { - npc.mode = SimulationMode::Loaded; - let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); - - emitter.emit(match NpcData::from_entity_info(entity_info) { - NpcData::Data { - pos, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - loot, - } => ServerEvent::CreateNpc { - pos, - npc: NpcBuilder::new(stats, body, alignment) - .with_skill_set(skill_set) - .with_health(health) - .with_poise(poise) - .with_inventory(inventory) - .with_agent(agent) - .with_scale(scale) - .with_loot(loot) - .with_rtsim(RtSimEntity(npc_id)), - }, - // 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!(), - }); - } - } - - // Synchronise rtsim NPC with entity data - for (pos, rtsim_vehicle) in (&positions, &rtsim_vehicles).join() { - data.npcs - .vehicles - .get_mut(rtsim_vehicle.0) - .filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) - .map(|vehicle| { - // Update rtsim NPC state - vehicle.wpos = pos.0; - }); - } - - // Synchronise rtsim NPC with entity data - for (pos, rtsim_entity, agent) in - (&positions, &rtsim_entities, (&mut agents).maybe()).join() - { - data.npcs - .get_mut(rtsim_entity.0) - .filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) - .map(|npc| { - // Update rtsim NPC state - npc.wpos = pos.0; - - // Update entity state - if let Some(agent) = agent { - agent.rtsim_controller.personality = npc.personality; - if let Some(action) = npc.action { - match action { - rtsim2::data::npc::NpcAction::Goto(wpos, sf) => { - agent.rtsim_controller.travel_to = Some(wpos); - agent.rtsim_controller.speed_factor = sf; - }, - } - } else { - agent.rtsim_controller.travel_to = None; - agent.rtsim_controller.speed_factor = 1.0; - } - } - }); - } - } -} diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 1e151dd792..a939bd113a 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -5,7 +5,7 @@ use crate::{ persistence::PersistedComponents, pet::restore_pet, presence::{Presence, RepositionOnChunkLoad}, - rtsim2::RtSim, + rtsim::RtSim, settings::Settings, sys::sentinel::DeletedEntities, wiring, BattleModeBuffer, SpawnPoint, diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index c9c5e8be78..d401221aae 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -10,7 +10,7 @@ use crate::{ chunk_serialize::ChunkSendEntry, client::Client, presence::{Presence, RepositionOnChunkLoad}, - rtsim2, + rtsim, settings::Settings, ChunkRequest, Tick, }; @@ -50,7 +50,7 @@ pub type TerrainPersistenceData<'a> = (); pub const SAFE_ZONE_RADIUS: f32 = 200.0; #[cfg(feature = "worldgen")] -type RtSimData<'a> = WriteExpect<'a, rtsim2::RtSim>; +type RtSimData<'a> = WriteExpect<'a, rtsim::RtSim>; #[cfg(not(feature = "worldgen"))] type RtSimData<'a> = ();