veloren/server/src/sys/terrain.rs
2022-09-08 15:43:53 -07:00

493 lines
17 KiB
Rust

#[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::ChunkSendEntry,
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<Write<'a, TerrainPersistence>>;
#[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<ServerEvent>>,
Read<'a, Tick>,
Read<'a, SpawnPoint>,
Read<'a, Settings>,
Read<'a, TimeOfDay>,
Read<'a, Calendar>,
ReadExpect<'a, SlowJobPool>,
ReadExpect<'a, IndexOwned>,
ReadExpect<'a, Arc<World>>,
ReadExpect<'a, EventBus<ChunkSendEntry>>,
WriteExpect<'a, ChunkGenerator>,
WriteExpect<'a, TerrainGrid>,
Write<'a, TerrainChanges>,
Write<'a, Vec<ChunkRequest>>,
WriteExpect<'a, RtSim>,
TerrainPersistenceData<'a>,
WriteStorage<'a, Pos>,
ReadStorage<'a, Presence>,
ReadStorage<'a, Client>,
Entities<'a>,
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<Self>,
(
server_event_bus,
tick,
spawn_point,
server_settings,
time_of_day,
calendar,
slow_jobs,
index,
world,
chunk_send_bus,
mut chunk_generator,
mut terrain,
mut terrain_changes,
mut chunk_requests,
mut rtsim,
mut _terrain_persistence,
mut positions,
presences,
clients,
entities,
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.gameplay.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_::<i32>())
.map(|x| x.as_::<f32>())
.unwrap_or_else(|| chunk.find_accessible_pos(pos.0.xy().as_::<i32>(), false));
repositioned.push(entity);
force_update
.get_mut(entity)
.map(|force_update| force_update.update());
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.
use rayon::iter::{IntoParallelIterator, ParallelIterator};
new_chunks.into_par_iter().for_each_init(
|| chunk_send_bus.emitter(),
|chunk_send_emitter, (key, _chunk)| {
(&entities, &presences, &positions, &clients)
.join()
.for_each(|(entity, presence, pos, _client)| {
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.terrain_view_distance.current().pow(2) {
chunk_send_emitter.emit(ChunkSendEntry {
entity,
chunk_key: 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.terrain_view_distance.current()) {
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<comp::Health>,
poise: comp::Poise,
inventory: comp::inventory::Inventory,
agent: Option<comp::Agent>,
body: comp::Body,
alignment: comp::Alignment,
scale: comp::Scale,
loot: LootSpec<String>,
},
Waypoint(Vec3<f32>),
}
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::with_loadout(loadout, body);
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_some(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<f32>,
chunk_pos: Vec2<i32>,
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<i32>, 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
}