diff --git a/common/src/comp/body/ship.rs b/common/src/comp/body/ship.rs index 9c7d2dc24b..97934165cc 100644 --- a/common/src/comp/body/ship.rs +++ b/common/src/comp/body/ship.rs @@ -168,6 +168,19 @@ impl Body { }, } } + + /// Max speed in block/s + pub fn get_speed(&self) -> f32 { + match self { + Body::DefaultAirship => 7.0, + Body::AirBalloon => 8.0, + Body::SailBoat => 5.0, + Body::Galleon => 6.0, + Body::Skiff => 6.0, + Body::Submarine => 4.0, + _ => 10.0, + } + } } /// Terrain is 11.0 scale relative to small-scale voxels, diff --git a/common/src/event.rs b/common/src/event.rs index bcfd379015..3280d41a55 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -12,7 +12,7 @@ use crate::{ lottery::LootSpec, mounting::VolumePos, outcome::Outcome, - rtsim::{RtSimEntity, RtSimVehicle}, + rtsim::RtSimEntity, terrain::SpriteKind, trade::{TradeAction, TradeId}, uid::Uid, @@ -225,13 +225,15 @@ pub enum ServerEvent { // TODO: to avoid breakage when adding new fields, perhaps have an `NpcBuilder` type? CreateNpc { pos: Pos, + ori: Ori, npc: NpcBuilder, + rider: Option, }, CreateShip { pos: Pos, ori: Ori, ship: comp::ship::Body, - rtsim_entity: Option, + rtsim_entity: Option, driver: Option, }, CreateWaypoint(Vec3), diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 6b48d8e2f5..9dd2cd4a84 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -17,8 +17,6 @@ use vek::*; slotmap::new_key_type! { pub struct NpcId; } -slotmap::new_key_type! { pub struct VehicleId; } - slotmap::new_key_type! { pub struct SiteId; } slotmap::new_key_type! { pub struct FactionId; } @@ -32,7 +30,7 @@ impl Component for RtSimEntity { type Storage = specs::VecStorage; } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum Actor { Npc(NpcId), Character(CharacterId), @@ -47,11 +45,12 @@ impl Actor { } } -#[derive(Copy, Clone, Debug)] -pub struct RtSimVehicle(pub VehicleId); +impl From for Actor { + fn from(value: NpcId) -> Self { Actor::Npc(value) } +} -impl Component for RtSimVehicle { - type Storage = specs::VecStorage; +impl From for Actor { + fn from(value: CharacterId) -> Self { Actor::Character(value) } } #[derive(EnumIter, Clone, Copy)] @@ -313,6 +312,8 @@ pub enum Role { Wild, #[serde(rename = "2")] Monster, + #[serde(rename = "2")] + Vehicle, } // Note: the `serde(name = "...")` is to minimise the length of field diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs index aab980cf90..5b174e943b 100644 --- a/common/src/states/basic_summon.rs +++ b/common/src/states/basic_summon.rs @@ -15,6 +15,7 @@ use crate::{ utils::*, }, terrain::Block, + util::Dir, vol::ReadVol, }; use rand::Rng; @@ -185,9 +186,11 @@ impl CharacterBehavior for Data { is_point: false, }); + let mut rng = rand::thread_rng(); // Send server event to create npc output_events.emit_server(ServerEvent::CreateNpc { pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z), + ori: comp::Ori::from(Dir::random_2d(&mut rng)), npc: NpcBuilder::new(stats, body, comp::Alignment::Owned(*data.uid)) .with_skill_set(skill_set) .with_health(health) @@ -204,6 +207,7 @@ impl CharacterBehavior for Data { .unwrap_or(comp::Scale(1.0)), ) .with_projectile(projectile), + rider: None, }); // Send local event used for frontend shenanigans diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 8bb5f8627a..bc08c65388 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -184,7 +184,10 @@ impl Body { // => 1 / (1 - drag).powi(2) - 1 = (dv / 30) / v // => 1 / (1 / (1 - drag).powi(2) - 1) = v / (dv / 30) // => (dv / 30) / (1 / (1 - drag).powi(2) - 1) = v - let v = (-self.base_accel() / 30.0) / ((1.0 - FRIC_GROUND).powi(2) - 1.0); + let v = match self { + Body::Ship(ship) => ship.get_speed(), + _ => (-self.base_accel() / 30.0) / ((1.0 - FRIC_GROUND).powi(2) - 1.0), + }; debug_assert!(v >= 0.0, "Speed must be positive!"); v } diff --git a/common/src/util/dir.rs b/common/src/util/dir.rs index 0cec04489b..162adfc97a 100644 --- a/common/src/util/dir.rs +++ b/common/src/util/dir.rs @@ -1,4 +1,5 @@ use super::{Plane, Projection}; +use rand::Rng; use serde::{Deserialize, Serialize}; use tracing::warn; use vek::*; @@ -79,6 +80,16 @@ impl Dir { }) } + /// Generates a random direction that has a z component of 0 + pub fn random_2d(rng: &mut impl Rng) -> Self { + Self::from_unnormalized(Vec3::new( + rng.gen_range(-1.0..=1.0), + rng.gen_range(-1.0..=1.0), + 0.0, + )) + .unwrap_or(Self::new(Vec3::unit_x())) + } + pub fn slerp(from: Self, to: Self, factor: f32) -> Self { Self(slerp_normalized(from.0, to.0, factor)) } diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index 0f466332d2..12a296493a 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -29,7 +29,7 @@ use std::{ /// Note that this number does *not* need incrementing on every change: most /// field removals/additions are fine. This number should only be incremented /// when we wish to perform a *hard purge* of rtsim data. -pub const CURRENT_VERSION: u32 = 4; +pub const CURRENT_VERSION: u32 = 5; #[derive(Clone, Serialize, Deserialize)] pub struct Data { diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 862d6a9ea3..1912667c61 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -10,7 +10,7 @@ use common::{ grid::Grid, rtsim::{ Actor, ChunkResource, FactionId, NpcAction, NpcActivity, NpcInput, Personality, ReportId, - Role, SiteId, VehicleId, + Role, SiteId, }, store::Id, terrain::CoordinateConversions, @@ -24,6 +24,7 @@ use std::{ collections::VecDeque, ops::{Deref, DerefMut}, }; +use tracing::error; use vek::*; use world::{ civ::Track, @@ -97,17 +98,17 @@ pub struct Brain { #[derive(Serialize, Deserialize)] pub struct Npc { + pub uid: u64, // Persisted state pub seed: u32, /// Represents the location of the NPC. pub wpos: Vec3, + pub dir: Vec2, pub body: comp::Body, pub role: Role, pub home: Option, pub faction: Option, - pub riding: Option, - pub is_dead: bool, /// The [`Report`]s that the NPC is aware of. @@ -143,12 +144,13 @@ pub struct Npc { impl Clone for Npc { fn clone(&self) -> Self { Self { + uid: self.uid, seed: self.seed, wpos: self.wpos, + dir: self.dir, role: self.role.clone(), home: self.home, faction: self.faction, - riding: self.riding.clone(), is_dead: self.is_dead, known_reports: self.known_reports.clone(), body: self.body, @@ -171,15 +173,17 @@ impl Npc { pub fn new(seed: u32, wpos: Vec3, body: comp::Body, role: Role) -> Self { Self { + // To be assigned later + uid: 0, seed, wpos, + dir: Vec2::unit_x(), body, personality: Default::default(), sentiments: Default::default(), role, home: None, faction: None, - riding: None, is_dead: false, known_reports: Default::default(), chunk_pos: None, @@ -213,24 +217,6 @@ impl Npc { self } - // TODO: have a dedicated `NpcBuilder` type for this. - pub fn steering(mut self, vehicle: impl Into>) -> Self { - self.riding = vehicle.into().map(|vehicle| Riding { - vehicle, - steering: true, - }); - self - } - - // TODO: have a dedicated `NpcBuilder` type for this. - pub fn riding(mut self, vehicle: impl Into>) -> Self { - self.riding = vehicle.into().map(|vehicle| Riding { - vehicle, - steering: false, - }); - self - } - // TODO: have a dedicated `NpcBuilder` type for this. pub fn with_faction(mut self, faction: impl Into>) -> Self { self.faction = faction.into(); @@ -246,7 +232,7 @@ impl Npc { pub fn profession(&self) -> Option { match &self.role { Role::Civilised(profession) => profession.clone(), - Role::Monster | Role::Wild => None, + Role::Monster | Role::Wild | Role::Vehicle => None, } } @@ -264,78 +250,218 @@ impl Npc { } } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Riding { - pub vehicle: VehicleId, - pub steering: bool, -} - -#[derive(Clone, Serialize, Deserialize)] -pub enum VehicleKind { - Airship, - Boat, -} - -// TODO: Merge into `Npc`? -#[derive(Clone, Serialize, Deserialize)] -pub struct Vehicle { - pub wpos: Vec3, - pub dir: Vec2, - - pub body: comp::ship::Body, - - #[serde(skip)] - pub chunk_pos: Option>, - - #[serde(skip)] - pub driver: Option, - - /// Whether the Vehicle is in simulated or loaded mode (when rtsim is run on - /// the server, loaded corresponds to being within a loaded chunk). When - /// in loaded mode, the interactions of the Vehicle should not be - /// simulated but should instead be derived from the game. - #[serde(skip)] - pub mode: SimulationMode, -} - -impl Vehicle { - pub fn new(wpos: Vec3, body: comp::ship::Body) -> Self { - Self { - wpos, - dir: Vec2::unit_y(), - body, - chunk_pos: None, - driver: None, - mode: SimulationMode::Simulated, - } - } - - pub fn get_body(&self) -> comp::Body { comp::Body::Ship(self.body) } - - /// Max speed in block/s - pub fn get_speed(&self) -> f32 { - match self.body { - comp::ship::Body::DefaultAirship => 7.0, - comp::ship::Body::AirBalloon => 8.0, - comp::ship::Body::SailBoat => 5.0, - comp::ship::Body::Galleon => 6.0, - comp::ship::Body::Skiff => 6.0, - comp::ship::Body::Submarine => 4.0, - _ => 10.0, - } - } -} - #[derive(Default, Clone, Serialize, Deserialize)] pub struct GridCell { pub npcs: Vec, - pub vehicles: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct NpcLink { + pub mount: NpcId, + pub rider: Actor, + pub is_steering: bool, +} + +#[derive(Clone, Default, Serialize, Deserialize)] +struct Riders { + steerer: Option, + riders: Vec, +} + +#[derive(Clone, Default, Serialize, Deserialize)] +#[serde( + from = "HopSlotMap", + into = "HopSlotMap" +)] +pub struct NpcLinks { + links: HopSlotMap, + mount_map: slotmap::SecondaryMap, + rider_map: HashMap, +} + +impl NpcLinks { + pub fn remove_mount(&mut self, mount: NpcId) { + if let Some(riders) = self.mount_map.remove(mount) { + for link in riders + .riders + .into_iter() + .chain(riders.steerer) + .filter_map(|link_id| self.links.get(link_id)) + { + self.rider_map.remove(&link.rider); + } + } + } + + /// Internal function, only removes from `mount_map`. + fn remove_rider(&mut self, id: MountId, link: &NpcLink) { + if let Some(riders) = self.mount_map.get_mut(link.mount) { + if link.is_steering && riders.steerer == Some(id) { + riders.steerer = None; + } else if let Some((i, _)) = riders.riders.iter().enumerate().find(|(_, i)| **i == id) { + riders.riders.remove(i); + } + + if riders.steerer.is_none() && riders.riders.is_empty() { + self.mount_map.remove(link.mount); + } + } + } + + pub fn remove_link(&mut self, link_id: MountId) { + if let Some(link) = self.links.remove(link_id) { + self.rider_map.remove(&link.rider); + self.remove_rider(link_id, &link); + } + } + + pub fn dismount(&mut self, rider: impl Into) { + if let Some(id) = self.rider_map.remove(&rider.into()) { + if let Some(link) = self.links.remove(id) { + self.remove_rider(id, &link); + } + } + } + + // This is the only function to actually add a mount link. + // And it ensures that there isn't link chaining + pub fn add_mounting( + &mut self, + mount: NpcId, + rider: impl Into, + steering: bool, + ) -> Result { + let rider = rider.into(); + if Actor::Npc(mount) == rider { + return Err(MountingError::MountSelf); + } + if let Actor::Npc(rider) = rider && self.mount_map.contains_key(rider) { + return Err(MountingError::RiderIsMounted); + } + if self.rider_map.contains_key(&Actor::Npc(mount)) { + return Err(MountingError::MountIsRiding); + } + if let Some(mount_entry) = self.mount_map.entry(mount) { + if let hashbrown::hash_map::Entry::Vacant(rider_entry) = self.rider_map.entry(rider) { + let riders = mount_entry.or_insert(Riders::default()); + + if steering { + if riders.steerer.is_none() { + let id = self.links.insert(NpcLink { + mount, + rider, + is_steering: true, + }); + riders.steerer = Some(id); + rider_entry.insert(id); + Ok(id) + } else { + Err(MountingError::HasSteerer) + } + } else { + // TODO: Maybe have some limit on the number of riders depending on the mount? + let id = self.links.insert(NpcLink { + mount, + rider, + is_steering: false, + }); + riders.riders.push(id); + rider_entry.insert(id); + Ok(id) + } + } else { + Err(MountingError::AlreadyRiding) + } + } else { + Err(MountingError::MountDead) + } + } + + pub fn steer( + &mut self, + mount: NpcId, + rider: impl Into, + ) -> Result { + self.add_mounting(mount, rider, true) + } + + pub fn ride( + &mut self, + mount: NpcId, + rider: impl Into, + ) -> Result { + self.add_mounting(mount, rider, false) + } + + pub fn get_mount_link(&self, rider: impl Into) -> Option<&NpcLink> { + self.rider_map + .get(&rider.into()) + .and_then(|link| self.links.get(*link)) + } + + pub fn get_steerer_link(&self, mount: NpcId) -> Option<&NpcLink> { + self.mount_map + .get(mount) + .and_then(|mount| self.links.get(mount.steerer?)) + } + + pub fn get(&self, id: MountId) -> Option<&NpcLink> { self.links.get(id) } + + pub fn ids(&self) -> impl Iterator + '_ { self.links.keys() } + + pub fn iter(&self) -> impl Iterator + '_ { self.links.values() } + + pub fn iter_mounts(&self) -> impl Iterator + '_ { self.mount_map.keys() } +} + +impl From> for NpcLinks { + fn from(mut value: HopSlotMap) -> Self { + let mut from_map = slotmap::SecondaryMap::new(); + let mut to_map = HashMap::with_capacity(value.len()); + let mut delete = Vec::new(); + for (id, link) in value.iter() { + if let Some(entry) = from_map.entry(link.mount) { + let riders = entry.or_insert(Riders::default()); + if link.is_steering { + if let Some(old) = riders.steerer.replace(id) { + error!("Replaced steerer {old:?} with {id:?}"); + } + } else { + riders.riders.push(id); + } + } else { + delete.push(id); + } + to_map.insert(link.rider, id); + } + for id in delete { + value.remove(id); + } + Self { + links: value, + mount_map: from_map, + rider_map: to_map, + } + } +} + +impl From for HopSlotMap { + fn from(other: NpcLinks) -> Self { other.links } +} +slotmap::new_key_type! { + pub struct MountId; +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct MountData { + is_steering: bool, } #[derive(Clone, Serialize, Deserialize)] pub struct Npcs { + pub uid_counter: u64, pub npcs: HopSlotMap, - pub vehicles: HopSlotMap, + pub mounts: NpcLinks, // TODO: This feels like it should be its own rtsim resource // TODO: Consider switching to `common::util::SpatialGrid` instead #[serde(skip, default = "construct_npc_grid")] @@ -347,8 +473,9 @@ pub struct Npcs { impl Default for Npcs { fn default() -> Self { Self { + uid_counter: 0, npcs: Default::default(), - vehicles: Default::default(), + mounts: Default::default(), npc_grid: construct_npc_grid(), character_map: Default::default(), } @@ -357,11 +484,22 @@ impl Default for Npcs { fn construct_npc_grid() -> Grid { Grid::new(Vec2::zero(), Default::default()) } -impl Npcs { - pub fn create_npc(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) } +#[derive(Debug)] +pub enum MountingError { + MountDead, + RiderDead, + HasSteerer, + AlreadyRiding, + MountIsRiding, + RiderIsMounted, + MountSelf, +} - pub fn create_vehicle(&mut self, vehicle: Vehicle) -> VehicleId { - self.vehicles.insert(vehicle) +impl Npcs { + pub fn create_npc(&mut self, mut npc: Npc) -> NpcId { + npc.uid = self.uid_counter; + self.uid_counter += 1; + self.npcs.insert(npc) } /// Queries nearby npcs, not garantueed to work if radius > 32.0 diff --git a/rtsim/src/event.rs b/rtsim/src/event.rs index 827fca56cc..4ba504013a 100644 --- a/rtsim/src/event.rs +++ b/rtsim/src/event.rs @@ -2,7 +2,7 @@ use crate::{RtState, Rule}; use common::{ mounting::VolumePos, resources::{Time, TimeOfDay}, - rtsim::{Actor, VehicleId}, + rtsim::{Actor, NpcId}, }; use vek::*; use world::{IndexRef, World}; @@ -41,6 +41,6 @@ impl Event for OnDeath {} #[derive(Clone)] pub struct OnMountVolume { pub actor: Actor, - pub pos: VolumePos, + pub pos: VolumePos, } impl Event for OnMountVolume {} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index d60956c04c..ec0b0c334a 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -4,13 +4,12 @@ pub mod site; use crate::data::{ faction::Faction, - npc::{Npc, Npcs, Profession, Vehicle}, + npc::{Npc, Npcs, Profession}, site::Site, Data, Nature, CURRENT_VERSION, }; use common::{ comp::{self, Body}, - grid::Grid, resources::TimeOfDay, rtsim::{Personality, Role, WorldSettings}, terrain::{BiomeKind, CoordinateConversions, TerrainChunkSize}, @@ -32,12 +31,7 @@ impl Data { let mut this = Self { version: CURRENT_VERSION, nature: Nature::generate(world), - npcs: Npcs { - npcs: Default::default(), - vehicles: Default::default(), - npc_grid: Grid::new(Vec2::zero(), Default::default()), - character_map: Default::default(), - }, + npcs: Npcs::default(), sites: Default::default(), factions: Default::default(), reports: Default::default(), @@ -185,13 +179,22 @@ impl Data { } } - if rng.gen_bool(0.4) { - let wpos = rand_wpos(&mut rng, matches_plazas) + Vec3::unit_z() * 50.0; - let vehicle_id = this - .npcs - .create_vehicle(Vehicle::new(wpos, comp::body::ship::Body::DefaultAirship)); + for plot in site2 + .plots + .values() + .filter(|plot| matches!(plot.kind(), PlotKind::AirshipDock(_))) + { + let wpos = site2.tile_center_wpos(plot.root_tile()); + let wpos = wpos.as_().with_z(world.sim().get_surface_alt_approx(wpos)) + + Vec3::unit_z() * 70.0; + let vehicle_id = this.npcs.create_npc(Npc::new( + rng.gen(), + wpos, + Body::Ship(comp::body::ship::Body::DefaultAirship), + Role::Vehicle, + )); - this.npcs.create_npc( + let npc_id = this.npcs.create_npc( Npc::new( rng.gen(), wpos, @@ -199,9 +202,12 @@ impl Data { Role::Civilised(Some(Profession::Captain)), ) .with_home(site_id) - .with_personality(Personality::random_good(&mut rng)) - .steering(vehicle_id), + .with_personality(Personality::random_good(&mut rng)), ); + this.npcs + .mounts + .steer(vehicle_id, npc_id) + .expect("We just created these npcs"); } } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 004065041e..a1d6f39d65 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -16,6 +16,7 @@ use crate::{ use common::{ astar::{Astar, PathResult}, comp::{ + self, compass::{Direction, Distance}, dialogue::Subject, Content, @@ -24,7 +25,7 @@ use common::{ rtsim::{Actor, ChunkResource, NpcInput, Profession, Role, SiteId}, spiral::Spiral2d, store::Id, - terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize}, + terrain::{CoordinateConversions, TerrainChunkSize}, time::DayPeriod, util::Dir, }; @@ -252,7 +253,7 @@ impl Rule for NpcAi { data.npcs .iter_mut() // Don't run AI for dead NPCs - .filter(|(_, npc)| !npc.is_dead) + .filter(|(_, npc)| !npc.is_dead && !matches!(npc.role, Role::Vehicle)) // Don't run AI for simulated NPCs every tick .filter(|(_, npc)| matches!(npc.mode, SimulationMode::Loaded) || (npc.seed as u64 + ctx.event.tick) % SIMULATED_TICK_SKIP == 0) .map(|(npc_id, npc)| { @@ -1011,27 +1012,35 @@ fn pilot(ship: common::comp::ship::Body) -> impl Action { // Travel between different towns in a straight line now(move |ctx, _| { let data = &*ctx.state.data(); - let site = data + let station_wpos = data .sites .iter() .filter(|(id, _)| Some(*id) != ctx.npc.current_site) - .filter(|(_, site)| { - site.world_site - .and_then(|site| ctx.index.sites.get(site).kind.convert_to_meta()) - .map_or(false, |meta| matches!(meta, SiteKindMeta::Settlement(_))) + .filter_map(|(_, site)| ctx.index.sites.get(site.world_site?).site2()) + .flat_map(|site| { + site.plots() + .filter(|plot| matches!(plot.kind(), PlotKind::AirshipDock(_))) + .map(|plot| site.tile_center_wpos(plot.root_tile())) }) .choose(&mut ctx.rng); - if let Some((_id, site)) = site { + if let Some(station_wpos) = station_wpos { Either::Right( goto_2d_flying( - site.wpos.as_(), + station_wpos.as_(), 1.0, 50.0, 150.0, 110.0, ship.flying_height(), ) - .then(goto_2d_flying(site.wpos.as_(), 1.0, 10.0, 32.0, 16.0, 10.0)), + .then(goto_2d_flying( + station_wpos.as_(), + 1.0, + 10.0, + 32.0, + 16.0, + 30.0, + )), ) } else { Either::Left(finish()) @@ -1157,15 +1166,17 @@ fn react_to_events(ctx: &mut NpcCtx, _: &mut S) -> Option impl Action { choose(|ctx, _| { - if let Some(riding) = &ctx.npc.riding { - if riding.steering { - if let Some(vehicle) = ctx.state.data().npcs.vehicles.get(riding.vehicle) { + if let Some(riding) = &ctx.state.data().npcs.mounts.get_mount_link(ctx.npc_id) { + if riding.is_steering { + if let Some(vehicle) = ctx.state.data().npcs.get(riding.mount) { match vehicle.body { - common::comp::ship::Body::DefaultAirship - | common::comp::ship::Body::AirBalloon => important(pilot(vehicle.body)), - common::comp::ship::Body::SailBoat | common::comp::ship::Body::Galleon => { - important(captain()) - }, + comp::Body::Ship( + body @ comp::ship::Body::DefaultAirship + | body @ comp::ship::Body::AirBalloon, + ) => important(pilot(body)), + comp::Body::Ship( + comp::ship::Body::SailBoat | comp::ship::Body::Galleon, + ) => important(captain()), _ => casual(idle()), } } else { @@ -1310,6 +1321,7 @@ fn think() -> impl Action { .l(), Role::Monster => monster().r().r().l(), Role::Wild => idle().r(), + Role::Vehicle => idle().r(), }, }) } diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 42cb4a04d0..c32faff891 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -1,6 +1,6 @@ use crate::{ data::{npc::SimulationMode, Npc}, - event::{EventCtx, OnDeath, OnMountVolume, OnSetup, OnTick}, + event::{EventCtx, OnDeath, OnMountVolume, OnTick}, RtState, Rule, RuleError, }; use common::{ @@ -12,6 +12,7 @@ use common::{ }; use rand::prelude::*; use rand_chacha::ChaChaRng; +use slotmap::SecondaryMap; use tracing::{error, warn}; use vek::{Clamp, Vec2}; use world::{site::SiteKind, CONFIG}; @@ -20,7 +21,6 @@ pub struct SimulateNpcs; impl Rule for SimulateNpcs { fn start(rtstate: &mut RtState) -> Result { - rtstate.bind(on_setup); rtstate.bind(on_death); rtstate.bind(on_tick); rtstate.bind(on_mount_volume); @@ -29,30 +29,14 @@ impl Rule for SimulateNpcs { } } -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_mut() { - if let Some(ride) = &npc.riding { - if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) { - let actor = Actor::Npc(npc_id); - if ride.steering && vehicle.driver.replace(actor).is_some() { - error!("Replaced driver"); - npc.riding = None; - } - } - } - } -} - fn on_mount_volume(ctx: EventCtx) { let data = &mut *ctx.state.data_mut(); + // TODO: Add actor to riders. if let VolumePos { kind: Volume::Entity(vehicle), .. } = ctx.event.pos - && let Some(vehicle) = data.npcs.vehicles.get(vehicle) - && let Some(Actor::Npc(driver)) = vehicle.driver - && let Some(driver) = data.npcs.get_mut(driver) { + && let Some(link) = data.npcs.mounts.get_steerer_link(vehicle) + && let Actor::Npc(driver) = link.rider + && let Some(driver) = data.npcs.get_mut(driver) { driver.controller.actions.push(NpcAction::Say(Some(ctx.event.actor), comp::Content::localized("npc-speech-welcome-aboard"))) } } @@ -71,8 +55,8 @@ fn on_death(ctx: EventCtx) { .sites .iter() .filter(|(id, site)| { + // Don't respawn in the same town Some(*id) != npc.home - && (npc.faction.is_none() || site.faction == npc.faction) && site.world_site.map_or(false, |s| { matches!( ctx.index.sites.get(s).kind, @@ -181,106 +165,37 @@ fn on_death(ctx: EventCtx) { fn on_tick(ctx: EventCtx) { let data = &mut *ctx.state.data_mut(); + + // Maintain links + let ids = data.npcs.mounts.ids().collect::>(); + let mut mount_activity = SecondaryMap::new(); + for link_id in ids { + if let Some(link) = data.npcs.mounts.get(link_id) { + if let Some(mount) = data + .npcs + .npcs + .get(link.mount) + .filter(|mount| !mount.is_dead) + { + let wpos = mount.wpos; + if let Actor::Npc(rider) = link.rider { + if let Some(rider) = + data.npcs.npcs.get_mut(rider).filter(|rider| !rider.is_dead) + { + rider.wpos = wpos; + mount_activity.insert(link.mount, rider.controller.activity); + } else { + data.npcs.mounts.dismount(link.rider) + } + } + } else { + data.npcs.mounts.remove_mount(link.mount) + } + } + } + for (npc_id, npc) in data.npcs.npcs.iter_mut().filter(|(_, npc)| !npc.is_dead) { 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 - vehicle.wpos; - let dist2 = diff.magnitude_squared(); - - if dist2 > 0.5f32.powi(2) { - let wpos = vehicle.wpos - + (diff - * (vehicle.get_speed() * speed_factor * ctx.event.dt - / dist2.sqrt()) - .min(1.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_().wpos_to_cpos(); - ctx.world - .sim() - .get(chunk_pos) - .map_or(true, |f| f.river.river_kind.is_some()) - }, - _ => false, - }; - - if is_valid { - vehicle.wpos = wpos; - } - vehicle.dir = (target.xy() - vehicle.wpos.xy()) - .try_normalized() - .unwrap_or(vehicle.dir); - } - }, - // When riding, other actions are disabled - Some( - NpcActivity::Goto(_, _) - | NpcActivity::Gather(_) - | NpcActivity::HuntAnimals - | NpcActivity::Dance(_) - | NpcActivity::Cheer(_) - | NpcActivity::Sit(_), - ) => {}, - 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(_) - | NpcActivity::Cheer(_) - | NpcActivity::Sit(_), - ) => { - // TODO: Maybe they should walk around randomly - // when gathering resources? - }, - None => {}, - } - // Make sure NPCs remain on the surface and within the map - npc.wpos = npc - .wpos - .xy() - .clamped( - Vec2::zero(), - (ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(), - ) - .with_z( - ctx.world - .sim() - .get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32)) - + npc.body.flying_height(), - ); - } - // Consume NPC actions for action in std::mem::take(&mut npc.controller.actions) { match action { @@ -288,6 +203,86 @@ fn on_tick(ctx: EventCtx) { NpcAction::Attack(_) => {}, // TODO: Implement simulated combat } } + + let activity = if data.npcs.mounts.get_mount_link(npc_id).is_some() { + // We are riding, nothing to do. + continue; + } else if let Some(activity) = mount_activity.get(npc_id) { + *activity + } else { + npc.controller.activity + }; + + match activity { + // Move NPCs if they have a target destination + Some(NpcActivity::Goto(target, speed_factor)) => { + let diff = target - npc.wpos; + let dist2 = diff.magnitude_squared(); + + if dist2 > 0.5f32.powi(2) { + let offset = diff + * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt + / dist2.sqrt()) + .min(1.0); + let new_wpos = npc.wpos + offset; + + let is_valid = match npc.body { + // Don't move water bound bodies outside of water. + Body::Ship(comp::ship::Body::SailBoat | comp::ship::Body::Galleon) + | Body::FishMedium(_) + | Body::FishSmall(_) => { + let chunk_pos = new_wpos.xy().as_().wpos_to_cpos(); + ctx.world + .sim() + .get(chunk_pos) + .map_or(true, |f| f.river.river_kind.is_some()) + }, + _ => true, + }; + + if is_valid { + npc.wpos = new_wpos; + } + + npc.dir = (target.xy() - npc.wpos.xy()) + .try_normalized() + .unwrap_or(npc.dir); + } + }, + Some( + NpcActivity::Gather(_) + | NpcActivity::HuntAnimals + | NpcActivity::Dance(_) + | NpcActivity::Cheer(_) + | NpcActivity::Sit(_), + ) => { + // TODO: Maybe they should walk around randomly + // when gathering resources? + }, + None => {}, + } + + // Make sure NPCs remain in a valid location + let clamped_wpos = npc.wpos.xy().clamped( + Vec2::zero(), + (ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(), + ); + match npc.body { + Body::Ship(comp::ship::Body::DefaultAirship | comp::ship::Body::AirBalloon) => { + npc.wpos = clamped_wpos.with_z( + ctx.world + .sim() + .get_surface_alt_approx(clamped_wpos.as_()) + .max(npc.wpos.z), + ); + }, + _ => { + npc.wpos = clamped_wpos.with_z( + ctx.world.sim().get_surface_alt_approx(clamped_wpos.as_()) + + npc.body.flying_height(), + ); + }, + } } // Move home if required @@ -305,31 +300,4 @@ fn on_tick(ctx: EventCtx) { npc.home = Some(new_home); } } - - for (id, vehicle) in data.npcs.vehicles.iter_mut() { - // Try to keep ships above ground and within the map - if matches!(vehicle.mode, SimulationMode::Simulated) { - vehicle.wpos = vehicle - .wpos - .xy() - .clamped( - Vec2::zero(), - (ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(), - ) - .with_z( - vehicle.wpos.z.max( - ctx.world - .sim() - .get_surface_alt_approx(vehicle.wpos.xy().as_()), - ), - ); - } - if let Some(Actor::Npc(driver)) = vehicle.driver - && data.npcs.npcs.get(driver).and_then(|driver| { - Some(driver.riding.as_ref()?.vehicle != id) - }).unwrap_or(true) - { - vehicle.driver = None; - } - } } diff --git a/rtsim/src/rule/sync_npcs.rs b/rtsim/src/rule/sync_npcs.rs index 76d55b1a0f..c28f682889 100644 --- a/rtsim/src/rule/sync_npcs.rs +++ b/rtsim/src/rule/sync_npcs.rs @@ -84,24 +84,6 @@ fn on_death(ctx: EventCtx) { 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_().wpos_to_cpos(); - 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 diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 11e1c3fb53..678ce6608d 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -674,7 +674,16 @@ fn handle_make_npc( } => { let mut entity_builder = server .state - .create_npc(pos, stats, skill_set, health, poise, inventory, body) + .create_npc( + pos, + comp::Ori::default(), + stats, + skill_set, + health, + poise, + inventory, + body, + ) .with(alignment) .with(scale) .with(comp::Vel(Vec3::new(0.0, 0.0, 0.0))); @@ -1303,9 +1312,8 @@ fn handle_rtsim_tp( action: &ServerChatCommand, ) -> CmdResult<()> { use crate::rtsim::RtSim; - let (npc_index, dismount_volume) = parse_cmd_args!(args, u32, bool); - let pos = if let Some(id) = npc_index { - // TODO: Take some other identifier than an integer to this command. + let (npc_id, dismount_volume) = parse_cmd_args!(args, u64, bool); + let pos = if let Some(id) = npc_id { server .state .ecs() @@ -1314,8 +1322,8 @@ fn handle_rtsim_tp( .data() .npcs .values() - .nth(id as usize) - .ok_or(action.help_string())? + .find(|npc| npc.uid == id) + .ok_or_else(|| format!("No NPC has the id {id}"))? .wpos } else { return Err(Content::Plain(action.help_string())); @@ -1335,15 +1343,14 @@ fn handle_rtsim_info( action: &ServerChatCommand, ) -> CmdResult<()> { use crate::rtsim::RtSim; - if let Some(id) = parse_cmd_args!(args, u32) { - // TODO: Take some other identifier than an integer to this command. + if let Some(id) = parse_cmd_args!(args, u64) { let rtsim = server.state.ecs().read_resource::(); let data = rtsim.state().data(); - let npc = data + let (id, npc) = data .npcs - .values() - .nth(id as usize) - .ok_or_else(|| format!("No NPC has index {}", id))?; + .iter() + .find(|(_, npc)| npc.uid == id) + .ok_or_else(|| format!("No NPC has the id {id}"))?; let mut info = String::new(); @@ -1360,7 +1367,10 @@ fn handle_rtsim_info( let _ = writeln!( &mut info, "Riding: {:?}", - npc.riding.as_ref().map(|riding| riding.vehicle) + data.npcs + .mounts + .get_mount_link(id) + .map(|link| data.npcs.get(link.mount).map_or(0, |mount| mount.uid)) ); let _ = writeln!(&mut info, "-- Action State --"); if let Some(brain) = &npc.brain { @@ -1404,8 +1414,7 @@ fn handle_rtsim_npc( let mut npcs = data .npcs .values() - .enumerate() - .filter(|(idx, npc)| { + .filter(|npc| { let mut tags = vec![ npc.profession() .map(|p| format!("{:?}", p)) @@ -1414,9 +1423,10 @@ fn handle_rtsim_npc( Role::Civilised(_) => "civilised".to_string(), Role::Wild => "wild".to_string(), Role::Monster => "monster".to_string(), + Role::Vehicle => "vehicle".to_string(), }, format!("{:?}", npc.mode), - format!("{}", idx), + format!("{}", npc.uid), npc_names[&npc.body].keyword.clone(), ]; if let Some(species_meta) = npc_names.get_species_meta(&npc.body) { @@ -1428,14 +1438,14 @@ fn handle_rtsim_npc( }) .collect::>(); if let Ok(pos) = position(server, target, "target") { - npcs.sort_by_key(|(_, npc)| (npc.wpos.distance_squared(pos.0) * 10.0) as u64); + npcs.sort_by_key(|npc| (npc.wpos.distance_squared(pos.0) * 10.0) as u64); } let mut info = String::new(); let _ = writeln!(&mut info, "-- NPCs matching [{}] --", terms.join(", ")); - for (idx, npc) in npcs.iter().take(count.unwrap_or(!0) as usize) { - let _ = write!(&mut info, "{} ({}), ", npc.get_name(), idx); + for npc in npcs.iter().take(count.unwrap_or(!0) as usize) { + let _ = write!(&mut info, "{} ({}), ", npc.get_name(), npc.uid); } let _ = writeln!(&mut info); let _ = writeln!( @@ -1600,6 +1610,7 @@ fn handle_spawn( .state .create_npc( pos, + comp::Ori::default(), comp::Stats::new(get_npc_name(id, npc::BodyType::from_body(body)), body), comp::SkillSet::default(), Some(comp::Health::new(body, 0)), @@ -1706,6 +1717,7 @@ fn handle_spawn_training_dummy( .state .create_npc( pos, + comp::Ori::default(), stats, skill_set, Some(health), diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 4976a3236f..15137a001a 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -17,7 +17,7 @@ use common::{ mounting::{Mounting, Volume, VolumeMounting, VolumePos}, outcome::Outcome, resources::{Secs, Time}, - rtsim::RtSimVehicle, + rtsim::RtSimEntity, uid::{IdMaps, Uid}, util::Dir, vol::IntoFullVolIterator, @@ -99,11 +99,18 @@ pub fn handle_loaded_character_data( server.notify_client(entity, result_msg); } -pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) -> EcsEntity { +pub fn handle_create_npc( + server: &mut Server, + pos: Pos, + ori: Ori, + mut npc: NpcBuilder, + rider: Option, +) -> EcsEntity { let entity = server .state .create_npc( pos, + ori, npc.stats, npc.skill_set, npc.health, @@ -168,8 +175,8 @@ pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) -> // Add to group system if a pet if let comp::Alignment::Owned(owner_uid) = npc.alignment { let state = server.state(); - let clients = state.ecs().read_storage::(); let uids = state.ecs().read_storage::(); + let clients = state.ecs().read_storage::(); let mut group_manager = state.ecs().write_resource::(); if let Some(owner) = state.ecs().entity_from_uid(owner_uid) { let map_markers = state.ecs().read_storage::(); @@ -207,6 +214,20 @@ pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) -> let _ = server.state.ecs().write_storage().insert(new_entity, group); } + if let Some(rider) = rider { + let rider_entity = handle_create_npc(server, pos, Ori::default(), rider, None); + let uids = server.state().ecs().read_storage::(); + let link = Mounting { + mount: *uids.get(new_entity).expect("We just created this entity"), + rider: *uids.get(rider_entity).expect("We just created this entity"), + }; + drop(uids); + server + .state + .link(link) + .expect("We just created these entities"); + } + new_entity } @@ -215,7 +236,7 @@ pub fn handle_create_ship( pos: Pos, ori: Ori, ship: comp::ship::Body, - rtsim_vehicle: Option, + rtsim_entity: Option, driver: Option, passengers: Vec, ) { @@ -252,13 +273,13 @@ pub fn handle_create_ship( entity = entity.with(agent); } */ - if let Some(rtsim_vehicle) = rtsim_vehicle { + if let Some(rtsim_vehicle) = rtsim_entity { entity = entity.with(rtsim_vehicle); } let entity = entity.build(); if let Some(driver) = driver { - let npc_entity = handle_create_npc(server, pos, driver); + let npc_entity = handle_create_npc(server, pos, ori, driver, None); let uids = server.state.ecs().read_storage::(); let (rider_uid, mount_uid) = uids @@ -292,7 +313,13 @@ pub fn handle_create_ship( } for passenger in passengers { - let npc_entity = handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passenger); + let npc_entity = handle_create_npc( + server, + Pos(pos.0 + Vec3::unit_z() * 5.0), + ori, + passenger, + None, + ); if let Some((rider_pos, rider_block)) = seats.next() { let uids = server.state.ecs().read_storage::(); let (rider_uid, mount_uid) = uids diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index 6ad46595ea..d843534409 100755 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -22,7 +22,7 @@ use common::{ link::Is, mounting::{Mount, Mounting, Rider, VolumeMounting, VolumePos, VolumeRider}, outcome::Outcome, - rtsim::RtSimVehicle, + rtsim::RtSimEntity, terrain::{Block, SpriteKind}, uid::{IdMaps, Uid}, vol::ReadVol, @@ -201,7 +201,7 @@ pub fn handle_mount_volume(server: &mut Server, rider: EcsEntity, volume_pos: Vo && let Some(rider_actor) = state.entity_as_actor(rider_entity) && let Some(volume_pos) = volume_pos.try_map_entity(|uid| { let entity = uid_allocator.uid_entity(uid)?; - state.read_storage::().get(entity).map(|v| v.0) + state.read_storage::().get(entity).map(|v| v.0) }) { state.ecs().write_resource::().hook_character_mount_volume( &state.ecs().read_resource::>(), diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 3d1520d430..1aa20f50b1 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -196,8 +196,13 @@ impl Server { ServerEvent::ExitIngame { entity } => { handle_exit_ingame(self, entity, false); }, - ServerEvent::CreateNpc { pos, npc } => { - handle_create_npc(self, pos, npc); + ServerEvent::CreateNpc { + pos, + ori, + npc, + rider, + } => { + handle_create_npc(self, pos, ori, npc, rider); }, ServerEvent::CreateShip { pos, diff --git a/server/src/lib.rs b/server/src/lib.rs index df46ee9b78..fe5ce2e79a 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -72,9 +72,11 @@ use common::{ cmd::ServerChatCommand, comp, event::{EventBus, ServerEvent}, + link::Is, + mounting::{Volume, VolumeRider}, region::RegionMap, resources::{BattleMode, GameMode, Time, TimeOfDay}, - rtsim::{RtSimEntity, RtSimVehicle}, + rtsim::RtSimEntity, shared_server_config::ServerConstants, slowjob::SlowJobPool, terrain::{TerrainChunk, TerrainChunkSize}, @@ -405,7 +407,6 @@ impl Server { state.ecs_mut().register::(); state.ecs_mut().register::(); state.ecs_mut().register::(); - state.ecs_mut().register::(); // Load banned words list let banned_words = settings.moderation.load_banned_words(data_dir); @@ -845,8 +846,6 @@ impl Server { self.state.delete_component::(entity); } - let mut rtsim = self.state.ecs().write_resource::(); - let rtsim_entities = self.state.ecs().read_storage::(); // Remove NPCs that are outside the view distances of all players // This is done by removing NPCs in unloaded chunks let to_delete = { @@ -856,16 +855,27 @@ impl Server { &self.state.ecs().read_storage::(), !&self.state.ecs().read_storage::(), self.state.ecs().read_storage::().maybe(), - rtsim_entities.maybe(), + self.state.ecs().read_storage::>().maybe(), ) .join() - .filter(|(_, pos, _, anchor, rtsim_entity)| { - if rtsim_entity.map_or(false, |rtsim_entity| { - !rtsim.can_unload_entity(*rtsim_entity) - }) { - return false; - } - let chunk_key = terrain.pos_key(pos.0.map(|e| e.floor() as i32)); + .filter(|(_, pos, _, anchor, is_volume_rider)| { + let pos = is_volume_rider + .and_then(|is_volume_rider| match is_volume_rider.pos.kind { + Volume::Terrain => None, + Volume::Entity(e) => { + let e = self.state.ecs().entity_from_uid(e)?; + let pos = self + .state + .ecs() + .read_storage::() + .get(e) + .copied()?; + + Some(pos.0) + }, + }) + .unwrap_or(pos.0); + let chunk_key = terrain.pos_key(pos.map(|e| e.floor() as i32)); match anchor { Some(Anchor::Chunk(hc)) => { // Check if both this chunk and the NPCs `home_chunk` is unloaded. If @@ -883,23 +893,16 @@ impl Server { .collect::>() }; + #[cfg(feature = "worldgen")] { - let rtsim_vehicles = self.state.ecs().read_storage::(); - - // Assimilate entities that are part of the real-time world simulation + let mut rtsim = self.state.ecs().write_resource::(); + let rtsim_entities = self.state.ecs().read_storage(); for entity in &to_delete { - #[cfg(feature = "worldgen")] if let Some(rtsim_entity) = rtsim_entities.get(*entity) { rtsim.hook_rtsim_entity_unload(*rtsim_entity); } - #[cfg(feature = "worldgen")] - if let Some(rtsim_vehicle) = rtsim_vehicles.get(*entity) { - rtsim.hook_rtsim_vehicle_unload(*rtsim_vehicle); - } } } - drop(rtsim_entities); - drop(rtsim); // Actually perform entity deletion for entity in to_delete { diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index 834c72ed15..df22e86ddc 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -6,7 +6,7 @@ use atomicwrites::{AtomicFile, OverwriteBehavior}; use common::{ grid::Grid, mounting::VolumePos, - rtsim::{Actor, ChunkResource, RtSimEntity, RtSimVehicle, VehicleId, WorldSettings}, + rtsim::{Actor, ChunkResource, NpcId, RtSimEntity, WorldSettings}, }; use common_ecs::{dispatch, System}; use common_state::BlockDiff; @@ -151,7 +151,7 @@ impl RtSim { &mut self, world: &World, index: IndexRef, - pos: VolumePos, + pos: VolumePos, actor: Actor, ) { self.state.emit(OnMountVolume { actor, pos }, world, index) @@ -177,32 +177,13 @@ impl RtSim { } pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) { - if let Some(npc) = self.state.get_data_mut().npcs.get_mut(entity.0) { - npc.mode = SimulationMode::Simulated; - } - } - - pub fn can_unload_entity(&self, entity: RtSimEntity) -> bool { - let data = self.state.data(); - data.npcs - .get(entity.0) - .and_then(|npc| { - let riding = npc.riding.as_ref()?; - let vehicle = data.npcs.vehicles.get(riding.vehicle)?; - Some(matches!(vehicle.mode, SimulationMode::Simulated)) - }) - .unwrap_or(true) - } - - pub fn hook_rtsim_vehicle_unload(&mut self, entity: RtSimVehicle) { let data = self.state.get_data_mut(); - if let Some(vehicle) = data.npcs.vehicles.get_mut(entity.0) { - vehicle.mode = SimulationMode::Simulated; - if let Some(Actor::Npc(npc)) = vehicle.driver { - if let Some(npc) = data.npcs.get_mut(npc) { - npc.mode = SimulationMode::Simulated; - } + + if let Some(npc) = data.npcs.get_mut(entity.0) { + if matches!(npc.mode, SimulationMode::Simulated) { + error!("Unloaded already unloaded entity"); } + npc.mode = SimulationMode::Simulated; } } diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 19e1c16642..fc8184bdb8 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -7,7 +7,7 @@ use common::{ event::{EventBus, NpcBuilder, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time, TimeOfDay}, - rtsim::{Actor, RtSimEntity, RtSimVehicle}, + rtsim::{Actor, NpcId, RtSimEntity}, slowjob::SlowJobPool, terrain::CoordinateConversions, trade::{Good, SiteInformation}, @@ -223,7 +223,6 @@ impl<'a> System<'a> for Sys { ReadExpect<'a, SlowJobPool>, ReadStorage<'a, comp::Pos>, ReadStorage<'a, RtSimEntity>, - ReadStorage<'a, RtSimVehicle>, WriteStorage<'a, comp::Agent>, ReadStorage<'a, Presence>, ); @@ -246,7 +245,6 @@ impl<'a> System<'a> for Sys { slow_jobs, positions, rtsim_entities, - rtsim_vehicles, mut agents, presences, ): Self::SystemData, @@ -294,14 +292,67 @@ impl<'a> System<'a> for Sys { let chunk_states = rtsim.state.resource::(); let data = &mut *rtsim.state.data_mut(); - // Load in vehicles - for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { - let chunk = vehicle.wpos.xy().as_::().wpos_to_cpos(); + let mut create_event = |id: NpcId, npc: &Npc, steering: Option| match npc.body { + Body::Ship(body) => { + emitter.emit(ServerEvent::CreateShip { + pos: comp::Pos(npc.wpos), + ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))), + ship: body, + rtsim_entity: Some(RtSimEntity(id)), + driver: steering, + }); + }, + _ => { + let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); - if matches!(vehicle.mode, SimulationMode::Simulated) + 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, + ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))), + npc: NpcBuilder::new(stats, body, alignment) + .with_skill_set(skill_set) + .with_health(health) + .with_poise(poise) + .with_inventory(inventory) + .with_agent(agent.map(|agent| Agent { + rtsim_outbox: Some(Default::default()), + ..agent + })) + .with_scale(scale) + .with_loot(loot) + .with_rtsim(RtSimEntity(id)), + rider: steering, + }, + // 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!(), + NpcData::Teleporter(_, _) => unimplemented!(), + }); + }, + }; + + // Load in mounted npcs and their riders + for mount in data.npcs.mounts.iter_mounts() { + let mount_npc = data.npcs.npcs.get_mut(mount).expect("This should exist"); + let chunk = mount_npc.wpos.xy().as_::().wpos_to_cpos(); + + if matches!(mount_npc.mode, SimulationMode::Simulated) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) { - vehicle.mode = SimulationMode::Loaded; + mount_npc.mode = SimulationMode::Loaded; let mut actor_info = |actor: Actor| { let npc_id = actor.npc()?; @@ -348,13 +399,14 @@ impl<'a> System<'a> for Sys { } }; - emitter.emit(ServerEvent::CreateShip { - pos: comp::Pos(vehicle.wpos), - ori: comp::Ori::from(Dir::new(vehicle.dir.with_z(0.0))), - ship: vehicle.body, - rtsim_entity: Some(RtSimVehicle(vehicle_id)), - driver: vehicle.driver.and_then(&mut actor_info), - }); + let steerer = data + .npcs + .mounts + .get_steerer_link(mount) + .and_then(|link| actor_info(link.rider)); + + let mount_npc = data.npcs.npcs.get(mount).expect("This should exist"); + create_event(mount, mount_npc, steerer); } } @@ -367,60 +419,13 @@ impl<'a> System<'a> for Sys { if matches!(npc.mode, SimulationMode::Simulated) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) // Riding npcs will be spawned by the vehicle. - && npc.riding.is_none() + && data.npcs.mounts.get_mount_link(npc_id).is_none() { 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.map(|agent| Agent { - rtsim_outbox: Some(Default::default()), - ..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!(), - NpcData::Teleporter(_, _) => unimplemented!(), - }); + create_event(npc_id, npc, None); } } - // 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; - }); - } - let mut emitter = server_event_bus.emitter(); // Synchronise rtsim NPC with entity data for (entity, pos, rtsim_entity, agent) in ( diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index cf89065227..fd6813230a 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -32,6 +32,7 @@ use common::{ rtsim::{Actor, RtSimEntity}, slowjob::SlowJobPool, uid::{IdMaps, Uid}, + util::Dir, LoadoutBuilder, ViewDistances, }; use common_net::{ @@ -52,6 +53,7 @@ pub trait StateExt { fn create_npc( &mut self, pos: comp::Pos, + ori: comp::Ori, stats: comp::Stats, skill_set: comp::SkillSet, health: Option, @@ -275,6 +277,7 @@ impl StateExt for State { fn create_npc( &mut self, pos: comp::Pos, + ori: comp::Ori, stats: comp::Stats, skill_set: comp::SkillSet, health: Option, @@ -286,14 +289,7 @@ impl StateExt for State { .create_entity_synced() .with(pos) .with(comp::Vel(Vec3::zero())) - .with( - comp::Ori::from_unnormalized_vec(Vec3::new( - thread_rng().gen_range(-1.0..1.0), - thread_rng().gen_range(-1.0..1.0), - 0.0, - )) - .unwrap_or_default(), - ) + .with(ori) .with(body.mass()) .with(body.density()) .with(body.collider()) @@ -787,10 +783,14 @@ impl StateExt for State { // This is the same as wild creatures naturally spawned in the world const DEFAULT_PET_HEALTH_LEVEL: u16 = 0; + let mut rng = rand::thread_rng(); + for (pet, body, stats) in pets { + let ori = comp::Ori::from(Dir::random_2d(&mut rng)); let pet_entity = self .create_npc( player_pos, + ori, stats, comp::SkillSet::default(), Some(comp::Health::new(body, DEFAULT_PET_HEALTH_LEVEL)), diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index fd8ca57589..5bdee93589 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -21,6 +21,7 @@ use common::{ resources::{Time, TimeOfDay}, slowjob::SlowJobPool, terrain::TerrainGrid, + util::Dir, SkillSetBuilder, }; @@ -137,6 +138,7 @@ impl<'a> System<'a> for Sys { ) }); + let mut rng = rand::thread_rng(); // Fetch any generated `TerrainChunk`s and insert them into the terrain. // Also, send the chunk data to anybody that is close by. let mut new_chunks = Vec::new(); @@ -211,6 +213,7 @@ impl<'a> System<'a> for Sys { } => { server_emitter.emit(ServerEvent::CreateNpc { pos, + ori: comp::Ori::from(Dir::random_2d(&mut rng)), npc: NpcBuilder::new(stats, body, alignment) .with_skill_set(skill_set) .with_health(health) @@ -220,6 +223,7 @@ impl<'a> System<'a> for Sys { .with_scale(scale) .with_anchor(comp::Anchor::Chunk(key)) .with_loot(loot), + rider: None, }); }, NpcData::Teleporter(pos, teleporter) => {