mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
493 lines
17 KiB
Rust
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
|
|
}
|