Removed old rtsim

This commit is contained in:
Joshua Barretto 2023-04-01 00:20:14 +01:00
parent 5062920b5c
commit 6035234c6e
18 changed files with 542 additions and 2328 deletions

View File

@ -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"}

View File

@ -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,

View File

@ -1193,7 +1193,7 @@ fn handle_rtsim_tp(
args: Vec<String>,
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<String>,
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::<RtSim>();
@ -1271,7 +1271,7 @@ fn handle_rtsim_purge(
args: Vec<String>,
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<String>,
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_::<i32>().wpos_to_cpos();
@ -2034,7 +2034,7 @@ fn handle_kill_npcs(
.get(entity)
.copied()
{
ecs.write_resource::<crate::rtsim2::RtSim>()
ecs.write_resource::<crate::rtsim::RtSim>()
.hook_rtsim_entity_delete(
&ecs.read_resource::<Arc<world::World>>(),
ecs.read_resource::<world::IndexOwned>().as_index_ref(),

View File

@ -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::<rtsim2::RtSim>()
.write_resource::<rtsim::RtSim>()
.hook_rtsim_entity_delete(
&state.ecs().read_resource::<Arc<world::World>>(),
state

View File

@ -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::<rtsim2::RtSim>().hook_block_update(
ecs.write_resource::<rtsim::RtSim>().hook_block_update(
&ecs.read_resource::<Arc<world::World>>(),
ecs.read_resource::<world::IndexOwned>().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::<rtsim2::RtSim>()
.write_resource::<rtsim::RtSim>()
.hook_rtsim_entity_unload(rtsim_entity);
}
#[cfg(feature = "worldgen")]
@ -884,7 +882,7 @@ impl Server {
{
self.state
.ecs()
.write_resource::<rtsim2::RtSim>()
.write_resource::<rtsim::RtSim>()
.hook_rtsim_vehicle_unload(rtsim_vehicle);
}
@ -1039,7 +1037,7 @@ impl Server {
let client = ecs.read_storage::<Client>();
let mut terrain = ecs.write_resource::<common::terrain::TerrainGrid>();
#[cfg(feature = "worldgen")]
let rtsim = ecs.read_resource::<rtsim2::RtSim>();
let rtsim = ecs.read_resource::<rtsim::RtSim>();
#[cfg(not(feature = "worldgen"))]
let rtsim = ();
@ -1222,7 +1220,7 @@ impl Server {
let ecs = self.state.ecs();
let slow_jobs = ecs.read_resource::<SlowJobPool>();
#[cfg(feature = "worldgen")]
let rtsim = ecs.read_resource::<rtsim2::RtSim>();
let rtsim = ecs.read_resource::<rtsim::RtSim>();
#[cfg(not(feature = "worldgen"))]
let rtsim = ();
ecs.write_resource::<ChunkGenerator>().generate_chunk(
@ -1467,10 +1465,7 @@ impl Drop for Server {
#[cfg(feature = "worldgen")]
{
info!("Saving rtsim state...");
self.state
.ecs()
.write_resource::<rtsim2::RtSim>()
.save(true);
self.state.ecs().write_resource::<rtsim::RtSim>().save(true);
}
}
}

View File

@ -1,34 +0,0 @@
use super::*;
use ::world::util::Grid;
pub struct Chunks {
chunks: Grid<Chunk>,
pub chunks_to_load: Vec<Vec2<i32>>,
pub chunks_to_unload: Vec<Vec2<i32>>,
}
impl Chunks {
pub fn new(size: Vec2<u32>) -> 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<i32>) -> Option<&Chunk> { self.chunks.get(key) }
pub fn size(&self) -> Vec2<u32> { self.chunks.size().map(|e| e as u32) }
pub fn chunk_mut(&mut self, key: Vec2<i32>) -> Option<&mut Chunk> { self.chunks.get_mut(key) }
pub fn chunk_at(&self, pos: Vec2<f32>) -> 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,
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
use common::terrain::Block;
use rtsim2::Event;
use rtsim::Event;
use vek::*;
#[derive(Clone)]

View File

@ -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<ServerEvent>>, 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<Self>, (_server_event_bus, mut rtsim): Self::SystemData) {
for _chunk in std::mem::take(&mut rtsim.chunks.chunks_to_load) {
// TODO
}
}
}

View File

@ -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<Entity>,
file_path: PathBuf,
last_saved: Option<Instant>,
state: RtState,
}
impl RtSim {
pub fn new(world_chunk_size: Vec2<u32>) -> 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<Self, ron::Error> {
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)."
);
}
pub fn hook_load_chunk(&mut self, key: Vec2<i32>) {
if let Some(chunk) = self.chunks.chunk_mut(key) {
if !chunk.is_loaded {
chunk.is_loaded = true;
self.chunks.chunks_to_load.push(key);
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<i32>, max_res: EnumMap<ChunkResource, usize>) {
if let Some(chunk_state) = self.state.resource_mut::<ChunkStates>().0.get_mut(key) {
*chunk_state = Some(LoadedChunkState { max_res });
}
}
pub fn hook_unload_chunk(&mut self, key: Vec2<i32>) {
if let Some(chunk) = self.chunks.chunk_mut(key) {
if chunk.is_loaded {
chunk.is_loaded = false;
self.chunks.chunks_to_unload.push(key);
}
}
}
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);
}
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 update_entity(&mut self, entity: RtSimId, pos: Vec3<f32>) {
self.entities.get_mut(entity).map(|e| e.pos = pos);
}
pub fn destroy_entity(&mut self, entity: RtSimId) {
// tracing::info!("Destroyed rtsim entity {}", entity);
self.entities.remove(entity);
}
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);
if let Some(chunk_state) = self.state.resource_mut::<ChunkStates>().0.get_mut(key) {
*chunk_state = None;
}
}
pub fn set_entity_mood(&mut self, entity: RtSimId, memory: Memory) {
self.entities
.get_mut(entity)
.map(|entity| entity.brain.set_mood(memory));
pub fn hook_block_update(
&mut self,
world: &World,
index: IndexRef,
wpos: Vec3<i32>,
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<dyn Error>)
.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<i32>) -> EnumMap<ChunkResource, f32> {
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<Option<LoadedChunkState>>);
pub struct LoadedChunkState {
// The maximum possible number of each resource in this chunk
pub max_res: EnumMap<ChunkResource, usize>,
}
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch::<unload_chunks::Sys>(dispatch_builder, &[]);
dispatch::<load_chunks::Sys>(dispatch_builder, &[&unload_chunks::Sys::sys_name()]);
dispatch::<tick::Sys>(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::<RtSimEntity>();
tracing::info!("Initiated real-time world simulation");
dispatch::<tick::Sys>(dispatch_builder, &[]);
}

View File

@ -1,6 +1,6 @@
pub mod deplete_resources;
use rtsim2::RtState;
use rtsim::RtState;
use tracing::info;
pub fn start_rules(rtstate: &mut RtState) {

View File

@ -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;

View File

@ -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<comp::agent::Mark> {
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<ServerEvent>>,
WriteExpect<'a, RtSim>,
ReadExpect<'a, TerrainGrid>,
ReadExpect<'a, Arc<world::World>>,
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<Self>,
(
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)
{
// 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;
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
.chunks
.chunk_at(entity.pos.xy())
.map(|c| c.is_loaded)
.unwrap_or(false)
.last_saved
.map_or(true, |ls| ls.elapsed() > Duration::from_secs(60))
{
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;
// TODO: Use slow jobs
let _ = slow_jobs;
rtsim.save(/* &slow_jobs, */ false);
}
if let Some(alt) = world
.sim()
.get_alt_approx(entity.pos.xy().map(|e| e.floor() as i32))
let chunk_states = rtsim.state.resource::<ChunkStates>();
let data = &mut *rtsim.state.data_mut();
for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() {
let chunk = vehicle.wpos.xy().as_::<i32>().wpos_to_cpos();
if matches!(vehicle.mode, SimulationMode::Simulated)
&& chunk_states.0.get(chunk).map_or(false, |c| c.is_some())
{
entity.pos.z = alt;
}
}
entity.tick(&time, &terrain, &world, &index.as_index_ref());
}
vehicle.mode = SimulationMode::Loaded;
// Tick entity AI each time if it's loaded
for (_, entity) in rtsim.entities.iter_mut().filter(|(_, e)| e.is_loaded) {
entity.last_time_ticked = time.0;
entity.tick(&time, &terrain, &world, &index.as_index_ref());
}
let mut 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());
let mut server_emitter = server_event_bus.emitter();
for id in to_reify {
rtsim.reify_entity(id);
let entity = &rtsim.entities[id];
let rtsim_entity = Some(RtSimEntity(id));
let body = entity.get_body();
let spawn_pos = terrain
.find_space(entity.pos.map(|e| e.floor() as i32))
.map(|e| e as f32)
+ Vec3::new(0.5, 0.5, body.flying_height());
let pos = comp::Pos(spawn_pos);
let event = if let comp::Body::Ship(ship) = body {
ServerEvent::CreateShip {
pos,
ship,
mountable: false,
agent: Some(comp::Agent::from_body(&body)),
rtsim_entity,
}
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 {
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);
error!("Npc is loaded but vehicle is unloaded");
None
}
match NpcData::from_entity_info(entity_info) {
};
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_::<i32>().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,
@ -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;
}
}
});
}
}

View File

@ -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<ServerEvent>>,
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<Self>,
(
_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
}
}
}

View File

@ -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<Instant>,
state: RtState,
}
impl RtSim {
pub fn new(
settings: &WorldSettings,
index: IndexRef,
world: &World,
data_dir: PathBuf,
) -> Result<Self, ron::Error> {
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<i32>, max_res: EnumMap<ChunkResource, usize>) {
if let Some(chunk_state) = self.state.resource_mut::<ChunkStates>().0.get_mut(key) {
*chunk_state = Some(LoadedChunkState { max_res });
}
}
pub fn hook_unload_chunk(&mut self, key: Vec2<i32>) {
if let Some(chunk_state) = self.state.resource_mut::<ChunkStates>().0.get_mut(key) {
*chunk_state = None;
}
}
pub fn hook_block_update(
&mut self,
world: &World,
index: IndexRef,
wpos: Vec3<i32>,
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<dyn Error>)
.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<i32>) -> EnumMap<ChunkResource, f32> {
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<Option<LoadedChunkState>>);
pub struct LoadedChunkState {
// The maximum possible number of each resource in this chunk
pub max_res: EnumMap<ChunkResource, usize>,
}
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch::<tick::Sys>(dispatch_builder, &[]);
}

View File

@ -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<comp::agent::Mark> {
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<ServerEvent>>,
WriteExpect<'a, RtSim>,
ReadExpect<'a, Arc<world::World>>,
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<Self>,
(
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::<ChunkStates>();
let data = &mut *rtsim.state.data_mut();
for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() {
let chunk = vehicle.wpos.xy().as_::<i32>().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_::<i32>().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;
}
}
});
}
}
}

View File

@ -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,

View File

@ -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> = ();