#[cfg(not(feature = "worldgen"))] use crate::test_world::{IndexOwned, World}; #[cfg(feature = "persistent_world")] use crate::TerrainPersistence; #[cfg(feature = "worldgen")] use world::{IndexOwned, World}; use crate::{ chunk_generator::ChunkGenerator, chunk_serialize::ChunkSendQueue, client::Client, presence::{Presence, RepositionOnChunkLoad}, rtsim::RtSim, settings::Settings, ChunkRequest, SpawnPoint, Tick, }; use common::{ calendar::Calendar, comp::{ self, agent, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate, Pos, Waypoint, }, event::{EventBus, ServerEvent}, generation::EntityInfo, lottery::LootSpec, resources::{Time, TimeOfDay}, slowjob::SlowJobPool, terrain::TerrainGrid, SkillSetBuilder, }; use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::ServerGeneral; use common_state::TerrainChanges; use comp::Behavior; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteExpect, WriteStorage}; use std::sync::Arc; use vek::*; #[cfg(feature = "persistent_world")] pub type TerrainPersistenceData<'a> = Option>; #[cfg(not(feature = "persistent_world"))] pub type TerrainPersistenceData<'a> = (); pub const SAFE_ZONE_RADIUS: f32 = 200.0; /// This system will handle loading generated chunks and unloading /// unneeded chunks. /// 1. Inserts newly generated chunks into the TerrainGrid /// 2. Sends new chunks to nearby clients /// 3. Handles the chunk's supplement (e.g. npcs) /// 4. Removes chunks outside the range of players #[derive(Default)] pub struct Sys; impl<'a> System<'a> for Sys { #[allow(clippy::type_complexity)] type SystemData = ( Read<'a, EventBus>, Read<'a, Tick>, Read<'a, SpawnPoint>, Read<'a, Settings>, Read<'a, TimeOfDay>, Read<'a, Calendar>, ReadExpect<'a, SlowJobPool>, ReadExpect<'a, IndexOwned>, ReadExpect<'a, Arc>, WriteExpect<'a, ChunkGenerator>, WriteExpect<'a, TerrainGrid>, Write<'a, TerrainChanges>, Write<'a, Vec>, WriteExpect<'a, RtSim>, TerrainPersistenceData<'a>, WriteStorage<'a, Pos>, ReadStorage<'a, Presence>, ReadStorage<'a, Client>, Entities<'a>, WriteStorage<'a, ChunkSendQueue>, WriteStorage<'a, RepositionOnChunkLoad>, WriteStorage<'a, ForceUpdate>, WriteStorage<'a, Waypoint>, ReadExpect<'a, Time>, ); const NAME: &'static str = "terrain"; const ORIGIN: Origin = Origin::Server; const PHASE: Phase = Phase::Create; fn run( _job: &mut Job, ( server_event_bus, tick, spawn_point, server_settings, time_of_day, calendar, slow_jobs, index, world, mut chunk_generator, mut terrain, mut terrain_changes, mut chunk_requests, mut rtsim, mut _terrain_persistence, mut positions, presences, clients, entities, mut chunk_send_queues, mut reposition_on_load, mut force_update, mut waypoints, time, ): Self::SystemData, ) { let mut server_emitter = server_event_bus.emitter(); // Generate requested chunks // // Submit requests for chunks right before receiving finished chunks so that we // don't create duplicate work for chunks that just finished but are not // yet added to the terrain. chunk_requests.drain(..).for_each(|request| { chunk_generator.generate_chunk( Some(request.entity), request.key, &slow_jobs, Arc::clone(&world), index.clone(), (*time_of_day, calendar.clone()), ) }); // 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(); 'insert_terrain_chunks: while let Some((key, res)) = chunk_generator.recv_new_chunk() { #[allow(unused_mut)] let (mut chunk, supplement) = match res { Ok((chunk, supplement)) => (chunk, supplement), Err(Some(entity)) => { if let Some(client) = clients.get(entity) { client.send_fallible(ServerGeneral::TerrainChunkUpdate { key, chunk: Err(()), }); } continue 'insert_terrain_chunks; }, Err(None) => { continue 'insert_terrain_chunks; }, }; // Apply changes from terrain persistence to this chunk #[cfg(feature = "persistent_world")] if let Some(terrain_persistence) = _terrain_persistence.as_mut() { terrain_persistence.apply_changes(key, &mut chunk); } // Arcify the chunk let chunk = Arc::new(chunk); // Add to list of chunks to send to nearby players. new_chunks.push((key, Arc::clone(&chunk))); // TODO: code duplication for chunk insertion between here and state.rs // Insert the chunk into terrain changes if terrain.insert(key, chunk).is_some() { terrain_changes.modified_chunks.insert(key); } else { terrain_changes.new_chunks.insert(key); rtsim.hook_load_chunk(key); } // Handle chunk supplement for entity in supplement.entities { // Check this because it's a common source of weird bugs assert!( terrain .pos_key(entity.pos.map(|e| e.floor() as i32)) .map2(key, |e, tgt| (e - tgt).abs() <= 1) .reduce_and(), "Chunk spawned entity that wasn't nearby", ); let data = NpcData::from_entity_info(entity); match data { NpcData::Waypoint(pos) => { server_emitter.emit(ServerEvent::CreateWaypoint(pos)); }, NpcData::Data { pos, stats, skill_set, health, poise, inventory, agent, body, alignment, scale, loot, } => { server_emitter.emit(ServerEvent::CreateNpc { pos, stats, skill_set, health, poise, inventory, agent, body, alignment, scale, anchor: Some(comp::Anchor::Chunk(key)), loot, rtsim_entity: None, projectile: None, }); }, } } // Insert a safezone if chunk contains the spawn position if server_settings.safe_spawn && is_spawn_chunk(key, *spawn_point, &terrain) { server_emitter.emit(ServerEvent::CreateSafezone { range: Some(SAFE_ZONE_RADIUS), pos: Pos(spawn_point.0), }); } } let mut repositioned = Vec::new(); for (entity, pos, _) in (&entities, &mut positions, &reposition_on_load).join() { // If an entity is marked as needing repositioning once the chunk loads (e.g. // from having just logged in), reposition them. let chunk_pos = terrain.pos_key(pos.0.map(|e| e as i32)); if let Some(chunk) = terrain.get_key(chunk_pos) { pos.0 = terrain .try_find_space(pos.0.as_::()) .map(|x| x.as_::()) .unwrap_or_else(|| chunk.find_accessible_pos(pos.0.xy().as_::(), false)); repositioned.push(entity); let _ = force_update.insert(entity, ForceUpdate); let _ = waypoints.insert(entity, Waypoint::new(pos.0, *time)); } } for entity in repositioned { reposition_on_load.remove(entity); } // Send the chunk to all nearby players. new_chunks.into_iter().for_each(|(key, _chunk)| { (&presences, &positions, &clients, &mut chunk_send_queues) .join() .for_each(|(presence, pos, _client, chunk_send_queue)| { let chunk_pos = terrain.pos_key(pos.0.map(|e| e as i32)); // Subtract 2 from the offset before computing squared magnitude // 1 since chunks need neighbors to be meshed // 1 to act as a buffer if the player moves in that direction let adjusted_dist_sqr = (chunk_pos - key) .map(|e: i32| (e.unsigned_abs()).saturating_sub(2)) .magnitude_squared(); if adjusted_dist_sqr <= presence.view_distance.pow(2) { chunk_send_queue.chunks.push(key); } }); }); // Remove chunks that are too far from players. let mut chunks_to_remove = Vec::new(); terrain .iter() .map(|(k, _)| k) // Don't check every chunk every tick (spread over 16 ticks) .filter(|k| k.x.unsigned_abs() % 4 + (k.y.unsigned_abs() % 4) * 4 == (tick.0 % 16) as u32) // There shouldn't be to many pending chunks so we will just check them all .chain(chunk_generator.pending_chunks()) .for_each(|chunk_key| { let mut should_drop = true; // For each player with a position, calculate the distance. for (presence, pos) in (&presences, &positions).join() { if chunk_in_vd(pos.0, chunk_key, &terrain, presence.view_distance) { should_drop = false; break; } } if should_drop { chunks_to_remove.push(chunk_key); } }); for key in chunks_to_remove { // Register the unloading of this chunk from terrain persistence #[cfg(feature = "persistent_world")] if let Some(terrain_persistence) = _terrain_persistence.as_mut() { terrain_persistence.unload_chunk(key); } // TODO: code duplication for chunk insertion between here and state.rs if terrain.remove(key).is_some() { terrain_changes.removed_chunks.insert(key); rtsim.hook_unload_chunk(key); } chunk_generator.cancel_if_pending(key); } } } /// Convinient structure to use when you need to create new npc /// from EntityInfo // TODO: better name? // TODO: if this is send around network, optimize the large_enum_variant #[allow(clippy::large_enum_variant)] pub enum NpcData { Data { pos: Pos, stats: comp::Stats, skill_set: comp::SkillSet, health: Option, poise: comp::Poise, inventory: comp::inventory::Inventory, agent: Option, body: comp::Body, alignment: comp::Alignment, scale: comp::Scale, loot: LootSpec, }, Waypoint(Vec3), } impl NpcData { pub fn from_entity_info(entity: EntityInfo) -> Self { let EntityInfo { // flags is_waypoint, has_agency, agent_mark, alignment, no_flee, // stats body, name, scale, pos, loot, // tools and skills skillset_asset, loadout: mut loadout_builder, inventory: items, make_loadout, trading_information: economy, // unused pet: _, // TODO: I had no idea we have this. } = entity; if is_waypoint { return Self::Waypoint(pos); } let name = name.unwrap_or_else(|| "Unnamed".to_string()); let stats = comp::Stats::new(name); let skill_set = { let skillset_builder = SkillSetBuilder::default(); if let Some(skillset_asset) = skillset_asset { skillset_builder.with_asset_expect(&skillset_asset).build() } else { skillset_builder.build() } }; let inventory = { // Evaluate lazy function for loadout creation if let Some(make_loadout) = make_loadout { loadout_builder = loadout_builder.with_creator(make_loadout, economy.as_ref()); } let loadout = loadout_builder.build(); let mut inventory = comp::inventory::Inventory::new_with_loadout(loadout); for (num, mut item) in items { if let Err(e) = item.set_amount(num) { tracing::warn!( "error during creating inventory for {name} at {pos}: {e:?}", name = &stats.name, ); } if let Err(e) = inventory.push(item) { tracing::warn!( "error during creating inventory for {name} at {pos}: {e:?}", name = &stats.name, ); } } inventory }; let health_level = skill_set .skill_level(skills::Skill::General(skills::GeneralSkill::HealthIncrease)) .unwrap_or(0); let health = Some(comp::Health::new(body, health_level)); let poise = comp::Poise::new(body); // Allow Humanoid, BirdMedium, and Parrot to speak let can_speak = match body { comp::Body::Humanoid(_) => true, comp::Body::BirdMedium(bird_medium) => match bird_medium.species { bird_medium::Species::Parrot => alignment == comp::Alignment::Npc, _ => false, }, _ => false, }; let trade_for_site = if matches!(agent_mark, Some(agent::Mark::Merchant)) { economy.map(|e| e.id) } else { None }; let agent = has_agency.then(|| { comp::Agent::from_body(&body) .with_behavior( Behavior::default() .maybe_with_capabilities(can_speak.then(|| BehaviorCapability::SPEAK)) .with_trade_site(trade_for_site), ) .with_patrol_origin(pos) .with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee) }); let agent = if matches!(alignment, comp::Alignment::Enemy) && matches!(body, comp::Body::Humanoid(_)) { agent.map(|a| a.with_aggro_no_warn().with_no_flee_if(true)) } else { agent }; NpcData::Data { pos: Pos(pos), stats, skill_set, health, poise, inventory, agent, body, alignment, scale: comp::Scale(scale), loot, } } } pub fn chunk_in_vd( player_pos: Vec3, chunk_pos: Vec2, terrain: &TerrainGrid, vd: u32, ) -> bool { // This fuzzy threshold prevents chunks rapidly unloading and reloading when // players move over a chunk border. const UNLOAD_THRESHOLD: u32 = 2; let player_chunk_pos = terrain.pos_key(player_pos.map(|e| e as i32)); let adjusted_dist_sqr = (player_chunk_pos - chunk_pos) .map(|e: i32| e.unsigned_abs()) .magnitude_squared(); adjusted_dist_sqr <= (vd.max(crate::MIN_VD) + UNLOAD_THRESHOLD).pow(2) } fn is_spawn_chunk(chunk_pos: Vec2, spawn_pos: SpawnPoint, terrain: &TerrainGrid) -> bool { let spawn_chunk_pos = terrain.pos_key(spawn_pos.0.map(|e| e as i32)); chunk_pos == spawn_chunk_pos }