1#[cfg(feature = "persistent_world")]
2use crate::TerrainPersistence;
3#[cfg(not(feature = "worldgen"))]
4use crate::test_world::{IndexOwned, World};
5use tracing::error;
6#[cfg(feature = "worldgen")]
7use world::{IndexOwned, World};
8
9#[cfg(feature = "worldgen")] use crate::rtsim;
10use crate::{
11 ChunkRequest, Tick, chunk_generator::ChunkGenerator, chunk_serialize::ChunkSendEntry,
12 client::Client, presence::RepositionOnChunkLoad, settings::Settings,
13};
14use common::{
15 SkillSetBuilder,
16 calendar::Calendar,
17 combat::DeathEffects,
18 comp::{
19 self, BehaviorCapability, ForceUpdate, Pos, Presence, Waypoint, agent, biped_small,
20 bird_medium,
21 },
22 event::{CreateNpcEvent, CreateSpecialEntityEvent, EmitExt, EventBus, NpcBuilder},
23 event_emitters,
24 generation::{EntityInfo, SpecialEntity},
25 lottery::LootSpec,
26 resources::{Time, TimeOfDay},
27 slowjob::SlowJobPool,
28 terrain::TerrainGrid,
29 util::Dir,
30};
31
32use common_ecs::{Job, Origin, Phase, System};
33use common_net::msg::ServerGeneral;
34use common_state::TerrainChanges;
35use comp::Behavior;
36use core::cmp::Reverse;
37use itertools::Itertools;
38use rayon::{iter::Either, prelude::*};
39use specs::{
40 Entities, Entity, Join, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write,
41 WriteExpect, WriteStorage, shred, storage::GenericReadStorage,
42};
43use std::{f32::consts::TAU, sync::Arc};
44use vek::*;
45
46#[cfg(feature = "persistent_world")]
47pub type TerrainPersistenceData<'a> = Option<Write<'a, TerrainPersistence>>;
48#[cfg(not(feature = "persistent_world"))]
49pub type TerrainPersistenceData<'a> = ();
50
51pub const SAFE_ZONE_RADIUS: f32 = 200.0;
52
53#[cfg(feature = "worldgen")]
54type RtSimData<'a> = WriteExpect<'a, rtsim::RtSim>;
55#[cfg(not(feature = "worldgen"))]
56type RtSimData<'a> = ();
57
58event_emitters! {
59 struct Events[Emitters] {
60 create_npc: CreateNpcEvent,
61 create_waypoint: CreateSpecialEntityEvent,
62 }
63}
64
65#[derive(SystemData)]
66pub struct Data<'a> {
67 events: Events<'a>,
68 tick: Read<'a, Tick>,
69 server_settings: Read<'a, Settings>,
70 time_of_day: Read<'a, TimeOfDay>,
71 calendar: Read<'a, Calendar>,
72 slow_jobs: ReadExpect<'a, SlowJobPool>,
73 index: ReadExpect<'a, IndexOwned>,
74 world: ReadExpect<'a, Arc<World>>,
75 chunk_send_bus: ReadExpect<'a, EventBus<ChunkSendEntry>>,
76 chunk_generator: WriteExpect<'a, ChunkGenerator>,
77 terrain: WriteExpect<'a, TerrainGrid>,
78 terrain_changes: Write<'a, TerrainChanges>,
79 chunk_requests: Write<'a, Vec<ChunkRequest>>,
80 rtsim: RtSimData<'a>,
81 #[cfg(feature = "persistent_world")]
82 terrain_persistence: TerrainPersistenceData<'a>,
83 positions: WriteStorage<'a, Pos>,
84 presences: ReadStorage<'a, Presence>,
85 clients: ReadStorage<'a, Client>,
86 entities: Entities<'a>,
87 reposition_on_load: WriteStorage<'a, RepositionOnChunkLoad>,
88 forced_updates: WriteStorage<'a, ForceUpdate>,
89 waypoints: WriteStorage<'a, Waypoint>,
90 time: ReadExpect<'a, Time>,
91}
92
93#[derive(Default)]
100pub struct Sys;
101impl<'a> System<'a> for Sys {
102 type SystemData = Data<'a>;
103
104 const NAME: &'static str = "terrain";
105 const ORIGIN: Origin = Origin::Server;
106 const PHASE: Phase = Phase::Create;
107
108 fn run(_job: &mut Job<Self>, mut data: Self::SystemData) {
109 let mut emitters = data.events.get_emitters();
110
111 data.chunk_requests.drain(..).for_each(|request| {
117 data.chunk_generator.generate_chunk(
118 Some(request.entity),
119 request.key,
120 &data.slow_jobs,
121 Arc::clone(&data.world),
122 &data.rtsim,
123 data.index.clone(),
124 (*data.time_of_day, data.calendar.clone()),
125 )
126 });
127
128 let mut rng = rand::thread_rng();
129 let mut new_chunks = Vec::new();
132 'insert_terrain_chunks: while let Some((key, res)) = data.chunk_generator.recv_new_chunk() {
133 #[cfg_attr(not(feature = "persistent_world"), expect(unused_mut))]
134 let (mut chunk, supplement) = match res {
135 Ok((chunk, supplement)) => (chunk, supplement),
136 Err(Some(entity)) => {
137 if let Some(client) = data.clients.get(entity) {
138 client.send_fallible(ServerGeneral::TerrainChunkUpdate {
139 key,
140 chunk: Err(()),
141 });
142 }
143 continue 'insert_terrain_chunks;
144 },
145 Err(None) => {
146 continue 'insert_terrain_chunks;
147 },
148 };
149
150 #[cfg(feature = "persistent_world")]
152 if let Some(terrain_persistence) = data.terrain_persistence.as_mut() {
153 terrain_persistence.apply_changes(key, &mut chunk);
154 }
155
156 let chunk = Arc::new(chunk);
158
159 new_chunks.push(key);
161
162 if data.terrain.insert(key, chunk).is_some() {
165 data.terrain_changes.modified_chunks.insert(key);
166 } else {
167 data.terrain_changes.new_chunks.insert(key);
168 #[cfg(feature = "worldgen")]
169 data.rtsim
170 .hook_load_chunk(key, supplement.rtsim_max_resources);
171 }
172
173 for entity in supplement.entities {
175 assert!(
177 data.terrain
178 .pos_key(entity.pos.map(|e| e.floor() as i32))
179 .map2(key, |e, tgt| (e - tgt).abs() <= 1)
180 .reduce_and(),
181 "Chunk spawned entity that wasn't nearby",
182 );
183
184 let data = SpawnEntityData::from_entity_info(entity);
185 match data {
186 SpawnEntityData::Special(pos, entity) => {
187 emitters.emit(CreateSpecialEntityEvent { pos, entity });
188 },
189 SpawnEntityData::Npc(data) => {
190 let (npc_builder, pos) = data.to_npc_builder();
191
192 emitters.emit(CreateNpcEvent {
193 pos,
194 ori: comp::Ori::from(Dir::random_2d(&mut rng)),
195 npc: npc_builder.with_anchor(comp::Anchor::Chunk(key)),
196 rider: None,
197 });
198 },
199 }
200 }
201 }
202
203 let repositioned = (&data.entities, &mut data.positions, (&mut data.forced_updates).maybe(), &data.reposition_on_load)
206 .par_join()
209 .filter_map(|(entity, pos, force_update, reposition)| {
210 let entity_pos = pos.0.map(|x| x as i32);
213 let chunk_pos = TerrainGrid::chunk_key(entity_pos);
216 let chunk = data.terrain.get_key(chunk_pos)?;
217 let new_pos = if reposition.needs_ground {
218 data.terrain.try_find_ground(entity_pos)
219 } else {
220 data.terrain.try_find_space(entity_pos)
221 }.map(|x| x.as_::<f32>()).unwrap_or_else(|| chunk.find_accessible_pos(entity_pos.xy(), false));
222 pos.0 = new_pos;
223 force_update.map(|force_update| force_update.update());
224 Some((entity, new_pos))
225 })
226 .collect::<Vec<_>>();
227
228 for (entity, new_pos) in repositioned {
229 if let Some(waypoint) = data.waypoints.get_mut(entity) {
230 *waypoint = Waypoint::new(new_pos, *data.time);
231 }
232 data.reposition_on_load.remove(entity);
233 }
234
235 let max_view_distance = data.server_settings.max_view_distance.unwrap_or(u32::MAX);
236 #[cfg(feature = "worldgen")]
237 let world_size = data.world.sim().get_size();
238 #[cfg(not(feature = "worldgen"))]
239 let world_size = data.world.map_size_lg().chunks().map(u32::from);
240 let (presences_position_entities, presences_positions) = prepare_player_presences(
241 world_size,
242 max_view_distance,
243 &data.entities,
244 &data.positions,
245 &data.presences,
246 &data.clients,
247 );
248 let real_max_view_distance = convert_to_loaded_vd(u32::MAX, max_view_distance);
249
250 new_chunks.par_iter().for_each_init(
252 || data.chunk_send_bus.emitter(),
253 |chunk_send_emitter, chunk_key| {
254 let min_chunk_x = chunk_key.x - real_max_view_distance;
262 let max_chunk_x = chunk_key.x + real_max_view_distance;
263 let start = presences_position_entities
264 .partition_point(|((pos, _), _)| i32::from(pos.x) < min_chunk_x);
265 let end = presences_position_entities
277 .partition_point(|((pos, _), _)| i32::from(pos.x) < max_chunk_x);
278 let interior = &presences_position_entities[start..end];
279 interior
280 .iter()
281 .filter(|((player_chunk_pos, player_vd_sqr), _)| {
282 chunk_in_vd(*player_chunk_pos, *player_vd_sqr, *chunk_key)
283 })
284 .for_each(|(_, entity)| {
285 chunk_send_emitter.emit(ChunkSendEntry {
286 entity: *entity,
287 chunk_key: *chunk_key,
288 });
289 });
290 },
291 );
292
293 let tick = (data.tick.0 % 16) as i32;
294
295 let chunks_to_remove = data.terrain
301 .par_keys()
302 .copied()
303 .chain(data.chunk_generator.par_pending_chunks())
306 .filter(|k| k.x % 4 + (k.y % 4) * 4 == tick)
317 .filter(|&chunk_key| {
318 let min_chunk_x = chunk_key.x - real_max_view_distance;
326 let max_chunk_x = chunk_key.x + real_max_view_distance;
327 let start = presences_positions
328 .partition_point(|(pos, _)| i32::from(pos.x) < min_chunk_x);
329 let end = presences_positions
339 .partition_point(|(pos, _)| i32::from(pos.x) < max_chunk_x);
340 let interior = &presences_positions[start..end];
341 !interior.iter().any(|&(player_chunk_pos, player_vd_sqr)| {
342 chunk_in_vd(player_chunk_pos, player_vd_sqr, chunk_key)
343 })
344 })
345 .collect::<Vec<_>>();
346
347 let chunks_to_remove = chunks_to_remove
348 .into_iter()
349 .filter_map(|key| {
350 #[cfg(feature = "persistent_world")]
352 if let Some(terrain_persistence) = data.terrain_persistence.as_mut() {
353 terrain_persistence.unload_chunk(key);
354 }
355
356 data.chunk_generator.cancel_if_pending(key);
357
358 data.terrain.remove(key).inspect(|_| {
364 data.terrain_changes.removed_chunks.insert(key);
365 })
366 })
367 .collect::<Vec<_>>();
368 if !chunks_to_remove.is_empty() {
369 data.slow_jobs.spawn("CHUNK_DROP", move || {
371 drop(chunks_to_remove);
372 });
373 }
374 }
375}
376
377#[derive(Debug)]
379pub struct NpcData {
380 pub pos: Pos,
381 pub stats: comp::Stats,
382 pub skill_set: comp::SkillSet,
383 pub health: Option<comp::Health>,
384 pub poise: comp::Poise,
385 pub inventory: comp::inventory::Inventory,
386 pub agent: Option<comp::Agent>,
387 pub body: comp::Body,
388 pub alignment: comp::Alignment,
389 pub scale: comp::Scale,
390 pub loot: LootSpec<String>,
391 pub pets: Vec<(NpcData, Vec3<f32>)>,
392 pub death_effects: Option<DeathEffects>,
393}
394
395#[derive(Debug)]
400pub enum SpawnEntityData {
401 Npc(NpcData),
402 Special(Vec3<f32>, SpecialEntity),
403}
404
405impl SpawnEntityData {
406 pub fn from_entity_info(entity: EntityInfo) -> Self {
407 let EntityInfo {
408 special_entity,
410 has_agency,
411 agent_mark,
412 alignment,
413 no_flee,
414 idle_wander_factor,
415 aggro_range_multiplier,
416 body,
418 name,
419 scale,
420 pos,
421 loot,
422 skillset_asset,
424 loadout: mut loadout_builder,
425 inventory: items,
426 make_loadout,
427 trading_information: economy,
428 pets,
429 death_effects,
430 } = entity;
431
432 if let Some(special) = special_entity {
433 return Self::Special(pos, special);
434 }
435
436 let name = name.unwrap_or_else(|| "Unnamed".to_string());
437 let stats = comp::Stats::new(name, body);
438
439 let skill_set = {
440 let skillset_builder = SkillSetBuilder::default();
441 if let Some(skillset_asset) = skillset_asset {
442 skillset_builder.with_asset_expect(&skillset_asset).build()
443 } else {
444 skillset_builder.build()
445 }
446 };
447
448 let inventory = {
449 if let Some(make_loadout) = make_loadout {
451 loadout_builder =
452 loadout_builder.with_creator(make_loadout, economy.as_ref(), None);
453 }
454 let loadout = loadout_builder.build();
455 let mut inventory = comp::inventory::Inventory::with_loadout(loadout, body);
456 for (num, mut item) in items {
457 if let Err(e) = item.set_amount(num) {
458 tracing::warn!(
459 "error during creating inventory for {name} at {pos}: {e:?}",
460 name = &stats.name,
461 );
462 }
463 if let Err(e) = inventory.push(item) {
464 tracing::warn!(
465 "error during creating inventory for {name} at {pos}: {e:?}",
466 name = &stats.name,
467 );
468 }
469 }
470
471 inventory
472 };
473
474 let health = Some(comp::Health::new(body));
475 let poise = comp::Poise::new(body);
476
477 let can_speak = match body {
479 comp::Body::Humanoid(_) => true,
480 comp::Body::BipedSmall(biped_small) => {
481 matches!(biped_small.species, biped_small::Species::Flamekeeper)
482 },
483 comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
484 bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
485 _ => false,
486 },
487 _ => false,
488 };
489
490 let trade_for_site = if matches!(agent_mark, Some(agent::Mark::Merchant)) {
491 economy.map(|e| e.id)
492 } else {
493 None
494 };
495
496 let agent = has_agency.then(|| {
497 let mut agent = comp::Agent::from_body(&body).with_behavior(
498 Behavior::default()
499 .maybe_with_capabilities(can_speak.then_some(BehaviorCapability::SPEAK))
500 .maybe_with_capabilities(trade_for_site.map(|_| BehaviorCapability::TRADE))
501 .with_trade_site(trade_for_site),
502 );
503
504 if !matches!(body, comp::Body::Humanoid(_)) {
506 agent = agent.with_patrol_origin(pos);
507 }
508
509 agent
510 .with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee)
511 .with_idle_wander_factor(idle_wander_factor)
512 .with_aggro_range_multiplier(aggro_range_multiplier)
513 });
514
515 let agent = if matches!(alignment, comp::Alignment::Enemy)
516 && matches!(body, comp::Body::Humanoid(_))
517 {
518 agent.map(|a| a.with_aggro_no_warn().with_no_flee_if(true))
519 } else {
520 agent
521 };
522
523 SpawnEntityData::Npc(NpcData {
524 pos: Pos(pos),
525 stats,
526 skill_set,
527 health,
528 poise,
529 inventory,
530 agent,
531 body,
532 alignment,
533 scale: comp::Scale(scale),
534 loot,
535 pets: {
536 let pet_count = pets.len() as f32;
537 pets.into_iter()
538 .enumerate()
539 .flat_map(|(i, pet)| {
540 Some((
541 SpawnEntityData::from_entity_info(pet)
542 .into_npc_data_inner()
543 .inspect_err(|data| {
544 error!("Pets must be SpawnEntityData::Npc, but found: {data:?}")
545 })
546 .ok()?,
547 Vec2::one()
548 .rotated_z(TAU * (i as f32 / pet_count))
549 .with_z(0.0)
550 * ((pet_count * 3.0) / TAU),
551 ))
552 })
553 .collect()
554 },
555 death_effects,
556 })
557 }
558
559 pub fn into_npc_data_inner(self) -> Result<NpcData, Self> {
560 match self {
561 SpawnEntityData::Npc(inner) => Ok(inner),
562 other => Err(other),
563 }
564 }
565}
566
567impl NpcData {
568 pub fn to_npc_builder(self) -> (NpcBuilder, comp::Pos) {
569 let NpcData {
570 pos,
571 stats,
572 skill_set,
573 health,
574 poise,
575 inventory,
576 agent,
577 body,
578 alignment,
579 scale,
580 loot,
581 pets,
582 death_effects,
583 } = self;
584
585 (
586 NpcBuilder::new(stats, body, alignment)
587 .with_skill_set(skill_set)
588 .with_health(health)
589 .with_poise(poise)
590 .with_inventory(inventory)
591 .with_agent(agent)
592 .with_scale(scale)
593 .with_loot(loot)
594 .with_pets(
595 pets.into_iter()
596 .map(|(pet, offset)| (pet.to_npc_builder().0, offset))
597 .collect::<Vec<_>>(),
598 )
599 .with_death_effects(death_effects),
600 pos,
601 )
602 }
603}
604
605pub fn convert_to_loaded_vd(vd: u32, max_view_distance: u32) -> i32 {
606 const MAX_VD: u32 = 1 << 7;
617
618 const UNLOAD_THRESHOLD: u32 = 2;
621
622 (vd.clamp(crate::MIN_VD, max_view_distance)
624 .saturating_add(UNLOAD_THRESHOLD))
625 .min(MAX_VD) as i32
626}
627
628fn prepare_for_vd_check(
630 world_aabr_in_chunks: &Aabr<i32>,
631 max_view_distance: u32,
632 entity: Entity,
633 presence: &Presence,
634 pos: &Pos,
635 client: Option<u32>,
636) -> Option<((Vec2<i16>, i32), Entity, bool)> {
637 let is_client = client.is_some();
638 let pos = pos.0;
639 let vd = presence.terrain_view_distance.current();
640
641 let player_pos = pos.map(|x| x as i32);
644 let player_chunk_pos = TerrainGrid::chunk_key(player_pos);
645 let player_vd = convert_to_loaded_vd(vd, max_view_distance);
646
647 let player_aabr_in_chunks = Aabr {
658 min: player_chunk_pos - player_vd,
659 max: player_chunk_pos + player_vd,
660 };
661
662 (world_aabr_in_chunks.max.x >= player_aabr_in_chunks.min.x &&
663 world_aabr_in_chunks.min.x <= player_aabr_in_chunks.max.x &&
664 world_aabr_in_chunks.max.y >= player_aabr_in_chunks.min.y &&
665 world_aabr_in_chunks.min.y <= player_aabr_in_chunks.max.y)
666 .then(|| ((player_chunk_pos.as_::<i16>(), player_vd.pow(2)), entity, is_client))
674}
675
676pub fn prepare_player_presences<'a, P>(
677 world_size: Vec2<u32>,
678 max_view_distance: u32,
679 entities: &Entities<'a>,
680 positions: P,
681 presences: &ReadStorage<'a, Presence>,
682 clients: &ReadStorage<'a, Client>,
683) -> (Vec<((Vec2<i16>, i32), Entity)>, Vec<(Vec2<i16>, i32)>)
684where
685 P: GenericReadStorage<Component = Pos> + Join<Type = &'a Pos>,
686{
687 let world_aabr_in_chunks = Aabr {
691 min: Vec2::zero(),
692 max: world_size.map(|x| x.saturating_sub(1)).as_::<i32>(),
694 };
695
696 let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) =
697 (entities, presences, positions, clients.mask().maybe())
698 .join()
699 .filter_map(|(entity, presence, position, client)| {
700 prepare_for_vd_check(
701 &world_aabr_in_chunks,
702 max_view_distance,
703 entity,
704 presence,
705 position,
706 client,
707 )
708 })
709 .partition_map(|(player_data, entity, is_client)| {
710 if is_client {
714 Either::Left((player_data, entity))
715 } else {
716 Either::Right(player_data)
719 }
720 });
721
722 presences_positions_entities
728 .sort_unstable_by_key(|&((pos, vd2), _)| (pos.x, pos.y, Reverse(vd2)));
729 presences_positions.sort_unstable_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
730 presences_positions.extend(
736 presences_positions_entities
737 .iter()
738 .map(|&(player_data, _)| player_data),
739 );
740 presences_positions.sort_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
744 presences_positions.dedup_by_key(|&mut (pos, _)| pos);
753
754 (presences_positions_entities, presences_positions)
755}
756
757pub fn chunk_in_vd(player_chunk_pos: Vec2<i16>, player_vd_sqr: i32, chunk_pos: Vec2<i32>) -> bool {
758 let adjusted_dist_sqr = (player_chunk_pos.as_::<i32>() - chunk_pos).magnitude_squared();
761
762 adjusted_dist_sqr <= player_vd_sqr
763}