diff --git a/Cargo.lock b/Cargo.lock index 591e2f7ab0..ac5f03837b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5402,7 +5402,6 @@ dependencies = [ "portpicker", "prometheus", "rand 0.7.3", - "rand_chacha 0.2.2", "ron", "scan_fmt", "serde", diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 3d4fa82a90..a9f077eea6 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -2,6 +2,7 @@ use crate::{ comp::{humanoid, quadruped_low, quadruped_medium, quadruped_small, Body}, path::Chaser, sync::Uid, + rtsim::RtSimController, }; use specs::{Component, Entity as EcsEntity}; use specs_idvs::IdvStorage; @@ -68,6 +69,7 @@ impl Component for Alignment { pub struct Psyche { pub aggro: f32, // 0.0 = always flees, 1.0 = always attacks, 0.5 = flee at 50% health } + impl<'a> From<&'a Body> for Psyche { fn from(body: &'a Body) -> Self { Self { @@ -137,6 +139,7 @@ impl<'a> From<&'a Body> for Psyche { #[derive(Clone, Debug, Default)] pub struct Agent { + pub rtsim_controller: RtSimController, pub patrol_origin: Option>, pub activity: Activity, /// Does the agent talk when e.g. hit by the player @@ -151,8 +154,7 @@ impl Agent { self } - pub fn new(origin: Vec3, can_speak: bool, body: &Body) -> Self { - let patrol_origin = Some(origin); + pub fn new(patrol_origin: Option>, can_speak: bool, body: &Body) -> Self { Agent { patrol_origin, can_speak, @@ -168,7 +170,10 @@ impl Component for Agent { #[derive(Clone, Debug)] pub enum Activity { - Idle(Vec2), + Idle { + bearing: Vec2, + chaser: Chaser, + }, Follow { target: EcsEntity, chaser: Chaser, @@ -189,5 +194,10 @@ impl Activity { } impl Default for Activity { - fn default() -> Self { Activity::Idle(Vec2::zero()) } + fn default() -> Self { + Activity::Idle { + bearing: Vec2::zero(), + chaser: Chaser::default(), + } + } } diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 9c4dd056e8..2b693e5590 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -1,9 +1,11 @@ // We'd like to not have this file in `common`, but sadly there are -// things in `common` that require it (currently, `ServerEvent`). When -// possible, this should be moved to the `rtsim` module in `server`. +// things in `common` that require it (currently, `ServerEvent` and +// `Agent`). When possible, this should be moved to the `rtsim` +// module in `server`. use specs_idvs::IdvStorage; use specs::Component; +use vek::*; pub type RtSimId = usize; @@ -13,3 +15,37 @@ pub struct RtSimEntity(pub RtSimId); impl Component for RtSimEntity { type Storage = IdvStorage; } + +/// This type is the map route through which the rtsim (real-time simulation) aspect +/// of the game communicates with the rest of the game. It is analagous to +/// `comp::Controller` in that it provides a consistent interface for simulation NPCs +/// to control their actions. Unlike `comp::Controller`, it is very abstract and is +/// intended for consumption by both the agent code and the internal rtsim simulation +/// code (depending on whether the entity is loaded into the game as a physical entity +/// or not). Agent code should attempt to act upon its instructions where reasonable +/// although deviations for various reasons (obstacle avoidance, counter-attacking, +/// etc.) are expected. +#[derive(Clone, Debug)] +pub struct RtSimController { + /// When this field is `Some(..)`, the agent should attempt to make progress + /// toward the given location, accounting for obstacles and other high-priority + /// situations like being attacked. + pub travel_to: Option>, + /// Proportion of full speed to move + pub speed_factor: f32, +} + +impl Default for RtSimController { + fn default() -> Self { + Self { + travel_to: None, + speed_factor: 1.0, + } + } +} + +impl RtSimController { + pub fn reset(&mut self) { + *self = Self::default(); + } +} diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index a2f5aec505..822d91a7e9 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -5,7 +5,7 @@ use crate::{ }, event::LocalEvent, states::*, - sys::{character_behavior::JoinData, phys::GRAVITY}, + sys::{character_behavior::JoinData, phys::{GRAVITY, FRIC_GROUND}}, util::Dir, }; use serde::{Deserialize, Serialize}; @@ -49,6 +49,25 @@ impl Body { } } + /// Attempt to determine the maximum speed of the character + /// when moving on the ground + pub fn max_speed_approx(&self) -> f32 { + // Inverse kinematics: at what velocity will acceleration + // be cancelled out by friction drag? + // Note: we assume no air (this is fine, current physics + // uses max(air_drag, ground_drag)). + // Derived via... + // v = (v + dv / 30) * (1 - drag).powf(2) (accel cancels drag) + // => 1 = (1 + (dv / 30) / v) * (1 - drag).powf(2) + // => 1 / (1 - drag).powf(2) = 1 + (dv / 30) / v + // => 1 / (1 - drag).powf(2) - 1 = (dv / 30) / v + // => 1 / (1 / (1 - drag).powf(2) - 1) = v / (dv / 30) + // => (dv / 30) / (1 / (1 - drag).powf(2) - 1) = v + let v = (-self.base_accel() / 30.0) / ((1.0 - FRIC_GROUND).powf(2.0) - 1.0); + debug_assert!(v >= 0.0, "Speed must be positive!"); + v + } + pub fn base_ori_rate(&self) -> f32 { match self { Body::Humanoid(_) => 20.0, diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index a1b6004c2d..4313851a92 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -212,40 +212,69 @@ impl<'a> System<'a> for Sys { 'activity: { match &mut agent.activity { - Activity::Idle(bearing) => { - *bearing += Vec2::new( - thread_rng().gen::() - 0.5, - thread_rng().gen::() - 0.5, - ) * 0.1 - - *bearing * 0.003 - - agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| { - (pos.0 - patrol_origin).xy() * 0.0002 - }); + Activity::Idle { bearing, chaser } => { + if let Some(travel_to) = agent.rtsim_controller.travel_to { + if let Some((bearing, speed)) = chaser.chase( + &*terrain, + pos.0, + vel.0, + travel_to, + TraversalConfig { + node_tolerance, + slow_factor, + on_ground: physics_state.on_ground, + min_tgt_dist: 1.25, + }, + ) { + inputs.move_dir = bearing + .xy() + .try_normalized() + .unwrap_or(Vec2::zero()) + * speed.min(agent.rtsim_controller.speed_factor); + inputs.jump.set_state(bearing.z > 1.5); + inputs.climb = Some(comp::Climb::Up); + inputs.move_z = bearing.z; + } + } else { + *bearing += Vec2::new( + thread_rng().gen::() - 0.5, + thread_rng().gen::() - 0.5, + ) * 0.1 + - *bearing * 0.003 + - agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| { + (pos.0 - patrol_origin).xy() * 0.0002 + }); - // Stop if we're too close to a wall - *bearing *= 0.1 - + if terrain - .ray( - pos.0 + Vec3::unit_z(), - pos.0 - + Vec3::from(*bearing) - .try_normalized() - .unwrap_or(Vec3::unit_y()) - * 5.0 - + Vec3::unit_z(), - ) - .until(Block::is_solid) - .cast() - .1 - .map_or(true, |b| b.is_none()) - { - 0.9 - } else { - 0.0 - }; + // Stop if we're too close to a wall + *bearing *= 0.1 + + if terrain + .ray( + pos.0 + Vec3::unit_z(), + pos.0 + + Vec3::from(*bearing) + .try_normalized() + .unwrap_or(Vec3::unit_y()) + * 5.0 + + Vec3::unit_z(), + ) + .until(Block::is_solid) + .cast() + .1 + .map_or(true, |b| b.is_none()) + { + 0.9 + } else { + 0.0 + }; - if bearing.magnitude_squared() > 0.5f32.powf(2.0) { - inputs.move_dir = *bearing * 0.65; + if bearing.magnitude_squared() > 0.5f32.powf(2.0) { + inputs.move_dir = *bearing * 0.65; + } + + // Sit + if thread_rng().gen::() < 0.0035 { + controller.actions.push(ControlAction::Sit); + } } // Put away weapon @@ -253,11 +282,6 @@ impl<'a> System<'a> for Sys { controller.actions.push(ControlAction::Unwield); } - // Sit - if thread_rng().gen::() < 0.0035 { - controller.actions.push(ControlAction::Sit); - } - // Sometimes try searching for new targets if thread_rng().gen::() < 0.1 { choose_target = true; @@ -621,7 +645,10 @@ impl<'a> System<'a> for Sys { } if do_idle { - agent.activity = Activity::Idle(Vec2::zero()); + agent.activity = Activity::Idle { + bearing: Vec2::zero(), + chaser: Chaser::default(), + }; } // Choose a new target to attack: only go out of our way to attack targets we diff --git a/common/src/sys/phys.rs b/common/src/sys/phys.rs index 937fc66cee..704c39339d 100644 --- a/common/src/sys/phys.rs +++ b/common/src/sys/phys.rs @@ -19,16 +19,16 @@ use std::ops::Range; use vek::*; pub const GRAVITY: f32 = 9.81 * 5.0; -const BOUYANCY: f32 = 1.0; +pub const BOUYANCY: f32 = 1.0; // Friction values used for linear damping. They are unitless quantities. The // value of these quantities must be between zero and one. They represent the // amount an object will slow down within 1/60th of a second. Eg. if the // friction is 0.01, and the speed is 1.0, then after 1/60th of a second the // speed will be 0.99. after 1 second the speed will be 0.54, which is 0.99 ^ // 60. -const FRIC_GROUND: f32 = 0.15; -const FRIC_AIR: f32 = 0.0125; -const FRIC_FLUID: f32 = 0.2; +pub const FRIC_GROUND: f32 = 0.15; +pub const FRIC_AIR: f32 = 0.0125; +pub const FRIC_FLUID: f32 = 0.2; // Integrates forces, calculates the new velocity based off of the old velocity // dt = delta time diff --git a/server/Cargo.toml b/server/Cargo.toml index 1b7f53d7e5..4a809c8d71 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -44,4 +44,3 @@ diesel = { version = "1.4.3", features = ["sqlite"] } diesel_migrations = "1.4.0" dotenv = "0.15.0" slab = "0.4" -rand_chacha = "0.2" diff --git a/server/src/lib.rs b/server/src/lib.rs index ff684b88ba..99f9e73c67 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -326,6 +326,7 @@ impl Server { // Insert the world into the ECS (todo: Maybe not an Arc?) let world = Arc::new(world); state.ecs_mut().insert(world.clone()); + state.ecs_mut().insert(index.clone()); // Set starting time for the server. state.ecs_mut().write_resource::().0 = settings.start_time; diff --git a/server/src/rtsim/chunks.rs b/server/src/rtsim/chunks.rs new file mode 100644 index 0000000000..93685f59a0 --- /dev/null +++ b/server/src/rtsim/chunks.rs @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000000..9d80644ed4 --- /dev/null +++ b/server/src/rtsim/entity.rs @@ -0,0 +1,93 @@ +use super::*; +use world::{ + util::RandomPerm, + World, + civ::{Site, Track}, +}; +use common::{ + terrain::TerrainGrid, + store::Id, + LoadoutBuilder, +}; + +pub struct Entity { + pub is_loaded: bool, + pub pos: Vec3, + pub seed: u32, + pub last_tick: u64, + pub controller: RtSimController, + + pub brain: Brain, +} + +const PERM_SPECIES: u32 = 0; +const PERM_BODY: u32 = 1; +const PERM_LOADOUT: u32 = 2; + +impl Entity { + pub fn rng(&self, perm: u32) -> impl Rng { + RandomPerm::new(self.seed + perm) + } + + pub fn get_body(&self) -> comp::Body { + let species = *(&comp::humanoid::ALL_SPECIES).choose(&mut self.rng(PERM_SPECIES)).unwrap(); + comp::humanoid::Body::random_with(&mut self.rng(PERM_BODY), &species).into() + } + + pub fn get_loadout(&self) -> comp::Loadout { + let main_tool = comp::Item::new_from_asset_expect((&[ + "common.items.weapons.sword.wood_sword", + "common.items.weapons.sword.starter_sword", + "common.items.weapons.sword.short_sword_0", + "common.items.weapons.bow.starter_bow", + "common.items.weapons.bow.leafy_longbow-0", + ]).choose(&mut self.rng(PERM_LOADOUT)).unwrap()); + LoadoutBuilder::build_loadout(self.get_body(), comp::Alignment::Npc, Some(main_tool), false) + .back(Some(comp::Item::new_from_asset_expect("common.items.armor.back.leather_adventurer"))) + .build() + } + + pub fn tick(&mut self, terrain: &TerrainGrid, world: &World) { + let tgt_site = self.brain.tgt.or_else(|| { + world.civs().sites + .iter() + .filter(|_| thread_rng().gen_range(0i32, 4) == 0) + .min_by_key(|(_, site)| { + let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + dist + if dist < 96 { + 100_000 + } else { + 0 + } + }) + .map(|(id, _)| id) + }); + self.brain.tgt = tgt_site; + + tgt_site.map(|tgt_site| { + let site = &world.civs().sites[tgt_site]; + + let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + + if dist < 64 { + self.brain.tgt = None; + } + + 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)) * 32.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); + self.controller.speed_factor = 0.75; + }); + } +} + +#[derive(Default)] +pub struct Brain { + tgt: Option>, + track: Option<(Track, usize)>, +} diff --git a/server/src/rtsim/load_chunks.rs b/server/src/rtsim/load_chunks.rs index 0007142874..8c71d67116 100644 --- a/server/src/rtsim/load_chunks.rs +++ b/server/src/rtsim/load_chunks.rs @@ -10,7 +10,7 @@ impl<'a> System<'a> for Sys { ); fn run(&mut self, (server_event_bus, mut rtsim): Self::SystemData) { - for chunk in std::mem::take(&mut rtsim.world.chunks_to_load) { + 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 2cde2b8b0f..81cd7e82a9 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -1,53 +1,55 @@ mod load_chunks; mod unload_chunks; mod tick; +mod entity; +mod chunks; use vek::*; -use world::util::Grid; use common::{ state::State, terrain::TerrainChunk, - rtsim::{RtSimEntity, RtSimId}, + rtsim::{RtSimEntity, RtSimId, RtSimController}, vol::RectRasterableVol, + comp, }; use specs::{DispatcherBuilder, WorldExt}; use specs_idvs::IdvStorage; use slab::Slab; use rand::prelude::*; +use self::{ + entity::Entity, + chunks::Chunks, +}; pub struct RtSim { - world: RtWorld, + tick: u64, + chunks: Chunks, entities: Slab, } impl RtSim { pub fn new(world_chunk_size: Vec2) -> Self { Self { - world: RtWorld { - chunks: Grid::populate_from(world_chunk_size.map(|e| e as i32), |_| Chunk { - is_loaded: false, - }), - chunks_to_load: Vec::new(), - chunks_to_unload: Vec::new(), - }, + tick: 0, + chunks: Chunks::new(world_chunk_size), entities: Slab::new(), } } pub fn hook_load_chunk(&mut self, key: Vec2) { - if let Some(chunk) = self.world.chunks.get_mut(key) { + if let Some(chunk) = self.chunks.chunk_mut(key) { if !chunk.is_loaded { chunk.is_loaded = true; - self.world.chunks_to_load.push(key); + self.chunks.chunks_to_load.push(key); } } } pub fn hook_unload_chunk(&mut self, key: Vec2) { - if let Some(chunk) = self.world.chunks.get_mut(key) { + if let Some(chunk) = self.chunks.chunk_mut(key) { if chunk.is_loaded { chunk.is_loaded = false; - self.world.chunks_to_unload.push(key); + self.chunks.chunks_to_unload.push(key); } } } @@ -72,28 +74,6 @@ impl RtSim { } } -pub struct RtWorld { - chunks: Grid, - chunks_to_load: Vec>, - chunks_to_unload: Vec>, -} - -impl RtWorld { - 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 { - is_loaded: bool, -} - -pub struct Entity { - is_loaded: bool, - pos: Vec3, - seed: u32, -} - const LOAD_CHUNK_SYS: &str = "rtsim_load_chunk_sys"; const UNLOAD_CHUNK_SYS: &str = "rtsim_unload_chunk_sys"; const TICK_SYS: &str = "rtsim_tick_sys"; @@ -107,16 +87,19 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { pub fn init(state: &mut State, world: &world::World) { let mut rtsim = RtSim::new(world.sim().get_size()); - for _ in 0..10 { - let pos = Vec2::new( - thread_rng().gen_range(0, rtsim.world.chunks.size().x * TerrainChunk::RECT_SIZE.x as i32), - thread_rng().gen_range(0, rtsim.world.chunks.size().y * TerrainChunk::RECT_SIZE.y as i32), + for _ in 0..10000 { + let pos = rtsim.chunks.size().map2( + TerrainChunk::RECT_SIZE, + |sz, chunk_sz| thread_rng().gen_range(0, sz * chunk_sz) as i32, ); let id = rtsim.entities.insert(Entity { is_loaded: false, pos: Vec3::from(pos.map(|e| e as f32)), seed: thread_rng().gen(), + controller: RtSimController::default(), + last_tick: 0, + brain: Default::default(), }); tracing::info!("Spawned rtsim NPC {} at {:?}", id, pos); diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index b037928b0a..bb11d793b0 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -2,49 +2,79 @@ use super::*; use common::{ event::{EventBus, ServerEvent}, terrain::TerrainGrid, - rtsim::RtSimEntity, + state::DeltaTime, comp, }; -use specs::{Join, Read, ReadStorage, System, Write, WriteExpect, ReadExpect}; -use rand_chacha::ChaChaRng; +use specs::{Join, Read, ReadStorage, WriteStorage, System, WriteExpect, ReadExpect}; use std::sync::Arc; +const ENTITY_TICK_PERIOD: u64 = 30; + pub struct Sys; impl<'a> System<'a> for Sys { type SystemData = ( + Read<'a, DeltaTime>, Read<'a, EventBus>, WriteExpect<'a, RtSim>, ReadExpect<'a, TerrainGrid>, ReadExpect<'a, Arc>, + ReadExpect<'a, world::IndexOwned>, ReadStorage<'a, comp::Pos>, ReadStorage<'a, RtSimEntity>, + WriteStorage<'a, comp::Agent>, ); fn run( &mut self, ( + dt, server_event_bus, mut rtsim, terrain, world, + index, positions, rtsim_entities, + mut agents, ): Self::SystemData, ) { let rtsim = &mut *rtsim; + rtsim.tick += 1; + if rtsim.tick % 300 == 0 { + if let Some((id, entity)) = rtsim.entities.iter().next() { + tracing::info!("Entity {} is at {:?}", id, entity.pos); + } + } // Update rtsim entities // TODO: don't update all of them each tick let mut to_reify = Vec::new(); for (id, entity) in rtsim.entities.iter_mut() { if entity.is_loaded { - continue; - } else if rtsim.world.chunk_at(entity.pos.xy()).map(|c| c.is_loaded).unwrap_or(false) { + // No load-specific behaviour yet + } else if rtsim.chunks.chunk_at(entity.pos.xy()).map(|c| c.is_loaded).unwrap_or(false) { to_reify.push(id); + } else { + // Simulate behaviour + if let Some(travel_to) = entity.controller.travel_to { + // Move towards target at approximate character speed + entity.pos += Vec3::from((travel_to.xy() - entity.pos.xy()) + .try_normalized() + .unwrap_or_else(Vec2::zero) + * entity.get_body().max_speed_approx() + * entity.controller.speed_factor) + * dt.0; + } + + if let Some(alt) = world.sim().get_alt_approx(entity.pos.xy().map(|e| e.floor() as i32)) { + entity.pos.z = alt; + } } - if let Some(chunk) = world.sim().get_wpos(entity.pos.xy().map(|e| e.floor() as i32)) { - entity.pos.z = chunk.alt; + // Tick entity AI + if entity.last_tick + ENTITY_TICK_PERIOD <= rtsim.tick { + entity.tick(&terrain, &world); + entity.last_tick = rtsim.tick; } } @@ -52,25 +82,15 @@ impl<'a> System<'a> for Sys { for id in to_reify { rtsim.reify_entity(id); let entity = &rtsim.entities[id]; - let mut rng = ChaChaRng::from_seed([ - entity.seed.to_le_bytes()[0], - entity.seed.to_le_bytes()[1], - entity.seed.to_le_bytes()[2], - entity.seed.to_le_bytes()[3], - 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - ]); - let species = *(&comp::humanoid::ALL_SPECIES).choose(&mut rng).unwrap(); - let body = comp::humanoid::Body::random_with(&mut rng, &species).into(); + 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, 0.0); + let body = entity.get_body(); server_emitter.emit(ServerEvent::CreateNpc { - pos: comp::Pos(terrain.find_space(entity.pos.map(|e| e.floor() as i32)).map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)), - stats: comp::Stats::new("Rtsim Entity".to_string(), body), + pos: comp::Pos(spawn_pos), + stats: comp::Stats::new("Traveller [rt]".to_string(), body), health: comp::Health::new(body, 10), - loadout: comp::Loadout::default(), + loadout: entity.get_loadout(), body, - agent: None, + agent: Some(comp::Agent::new(None, true, &body)), alignment: comp::Alignment::Npc, scale: comp::Scale(1.0), drop_item: None, @@ -80,8 +100,11 @@ impl<'a> System<'a> for Sys { } // Update rtsim with real entity data - for (pos, rtsim_entity) in (&positions, &rtsim_entities).join() { - rtsim.entities.get_mut(rtsim_entity.0).map(|entity| entity.pos = pos.0); + for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, &mut agents).join() { + rtsim.entities.get_mut(rtsim_entity.0).map(|entity| { + entity.pos = pos.0; + agent.rtsim_controller = entity.controller.clone(); + }); } } } diff --git a/server/src/rtsim/unload_chunks.rs b/server/src/rtsim/unload_chunks.rs index 7b7fd6dcff..a9f9506aef 100644 --- a/server/src/rtsim/unload_chunks.rs +++ b/server/src/rtsim/unload_chunks.rs @@ -28,7 +28,7 @@ impl<'a> System<'a> for Sys { positions, ): Self::SystemData, ) { - let chunks = std::mem::take(&mut rtsim.world.chunks_to_unload); + let chunks = std::mem::take(&mut rtsim.chunks.chunks_to_unload); for chunk in chunks { // TODO diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index f42716bf43..be8d3cab4a 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -189,7 +189,7 @@ impl<'a> System<'a> for Sys { health, loadout, agent: if entity.has_agency { - Some(comp::Agent::new(entity.pos, can_speak, &body)) + Some(comp::Agent::new(Some(entity.pos), can_speak, &body)) } else { None }, diff --git a/world/src/block/mod.rs b/world/src/block/mod.rs index e64760b4fa..5d4103c64b 100644 --- a/world/src/block/mod.rs +++ b/world/src/block/mod.rs @@ -209,8 +209,8 @@ pub fn block_from_structure( ) -> Option { let field = RandomField::new(structure_seed); - let lerp = ((field.get(Vec3::from(structure_pos)).rem_euclid(256)) as f32 / 255.0) * 0.85 - + ((field.get(pos + std::i32::MAX / 2).rem_euclid(256)) as f32 / 255.0) * 0.15; + let lerp = ((field.get(Vec3::from(structure_pos)).rem_euclid(256)) as f32 / 255.0) * 0.8 + + ((field.get(pos + std::i32::MAX / 2).rem_euclid(256)) as f32 / 255.0) * 0.2; match sblock { StructureBlock::None => None, diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index af33cfef87..73271f74db 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -41,21 +41,21 @@ const fn initial_civ_count(map_size_lg: MapSizeLg) -> u32 { #[allow(clippy::type_complexity)] // TODO: Pending review in #587 #[derive(Default)] pub struct Civs { - civs: Store, - places: Store, + pub civs: Store, + pub places: Store, - tracks: Store, + pub tracks: Store, /// We use this hasher (FxHasher64) because /// (1) we don't care about DDOS attacks (ruling out SipHash); /// (2) we care about determinism across computers (ruling out AAHash); /// (3) we have 8-byte keys (for which FxHash is fastest). - track_map: HashMap< + pub track_map: HashMap< Id, HashMap, Id, BuildHasherDefault>, BuildHasherDefault, >, - sites: Store, + pub sites: Store, } // Change this to get rid of particularly horrid seeds diff --git a/world/src/util/random.rs b/world/src/util/random.rs index f5c9ab3cd5..dd2d0c2ee1 100644 --- a/world/src/util/random.rs +++ b/world/src/util/random.rs @@ -1,4 +1,5 @@ use super::{seed_expan, Sampler}; +use rand::RngCore; use vek::*; #[derive(Clone, Copy)] @@ -59,3 +60,26 @@ impl Sampler<'static> for RandomPerm { seed_expan::diffuse_mult(&[self.seed, perm]) } } + +// `RandomPerm` is not high-quality but it is at least fast and deterministic. +impl RngCore for RandomPerm { + fn next_u32(&mut self) -> u32 { + self.seed = self.get(self.seed); + self.seed + } + + fn next_u64(&mut self) -> u64 { + let a = self.next_u32(); + let b = self.next_u32(); + (a as u64) << 32 | b as u64 + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + dest.iter_mut().for_each(|b| *b = (self.next_u32() & 0xFF) as u8); + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> { + self.fill_bytes(dest); + Ok(()) + } +}