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, RiderEffects},
18 comp::{
19 self, BehaviorCapability, Content, ForceUpdate, Pos, Presence, Waypoint, agent,
20 biped_small, 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::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, &data.world);
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 });
197 },
198 }
199 }
200 }
201
202 let repositioned = (&data.entities, &mut data.positions, (&mut data.forced_updates).maybe(), &data.reposition_on_load)
205 .par_join()
208 .filter_map(|(entity, pos, force_update, reposition)| {
209 let entity_pos = pos.0.map(|x| x as i32);
212 let chunk_pos = TerrainGrid::chunk_key(entity_pos);
215 let chunk = data.terrain.get_key(chunk_pos)?;
216 let new_pos = if reposition.needs_ground {
217 data.terrain.try_find_ground(entity_pos)
218 } else {
219 data.terrain.try_find_space(entity_pos)
220 }.map(|x| x.as_::<f32>()).unwrap_or_else(|| chunk.find_accessible_pos(entity_pos.xy(), false));
221 pos.0 = new_pos;
222 force_update.map(|force_update| force_update.update());
223 Some((entity, new_pos))
224 })
225 .collect::<Vec<_>>();
226
227 for (entity, new_pos) in repositioned {
228 if let Some(waypoint) = data.waypoints.get_mut(entity) {
229 *waypoint = Waypoint::new(new_pos, *data.time);
230 }
231 data.reposition_on_load.remove(entity);
232 }
233
234 let max_view_distance = data.server_settings.max_view_distance.unwrap_or(u32::MAX);
235 #[cfg(feature = "worldgen")]
236 let world_size = data.world.sim().get_size();
237 #[cfg(not(feature = "worldgen"))]
238 let world_size = data.world.map_size_lg().chunks().map(u32::from);
239 let (presences_position_entities, presences_positions) = prepare_player_presences(
240 world_size,
241 max_view_distance,
242 &data.entities,
243 &data.positions,
244 &data.presences,
245 &data.clients,
246 );
247 let real_max_view_distance = convert_to_loaded_vd(u32::MAX, max_view_distance);
248
249 new_chunks.par_iter().for_each_init(
251 || data.chunk_send_bus.emitter(),
252 |chunk_send_emitter, chunk_key| {
253 let min_chunk_x = chunk_key.x - real_max_view_distance;
261 let max_chunk_x = chunk_key.x + real_max_view_distance;
262 let start = presences_position_entities
263 .partition_point(|((pos, _), _)| i32::from(pos.x) < min_chunk_x);
264 let end = presences_position_entities
276 .partition_point(|((pos, _), _)| i32::from(pos.x) < max_chunk_x);
277 let interior = &presences_position_entities[start..end];
278 interior
279 .iter()
280 .filter(|((player_chunk_pos, player_vd_sqr), _)| {
281 chunk_in_vd(*player_chunk_pos, *player_vd_sqr, *chunk_key)
282 })
283 .for_each(|(_, entity)| {
284 chunk_send_emitter.emit(ChunkSendEntry {
285 entity: *entity,
286 chunk_key: *chunk_key,
287 });
288 });
289 },
290 );
291
292 let tick = (data.tick.0 % 16) as i32;
293
294 let chunks_to_remove = data.terrain
300 .par_keys()
301 .copied()
302 .chain(data.chunk_generator.par_pending_chunks())
305 .filter(|k| k.x % 4 + (k.y % 4) * 4 == tick)
316 .filter(|&chunk_key| {
317 let min_chunk_x = chunk_key.x - real_max_view_distance;
325 let max_chunk_x = chunk_key.x + real_max_view_distance;
326 let start = presences_positions
327 .partition_point(|(pos, _)| i32::from(pos.x) < min_chunk_x);
328 let end = presences_positions
338 .partition_point(|(pos, _)| i32::from(pos.x) < max_chunk_x);
339 let interior = &presences_positions[start..end];
340 !interior.iter().any(|&(player_chunk_pos, player_vd_sqr)| {
341 chunk_in_vd(player_chunk_pos, player_vd_sqr, chunk_key)
342 })
343 })
344 .collect::<Vec<_>>();
345
346 let chunks_to_remove = chunks_to_remove
347 .into_iter()
348 .filter_map(|key| {
349 #[cfg(feature = "persistent_world")]
351 if let Some(terrain_persistence) = data.terrain_persistence.as_mut() {
352 terrain_persistence.unload_chunk(key);
353 }
354
355 data.chunk_generator.cancel_if_pending(key);
356
357 data.terrain.remove(key).inspect(|_| {
363 data.terrain_changes.removed_chunks.insert(key);
364 })
365 })
366 .collect::<Vec<_>>();
367 if !chunks_to_remove.is_empty() {
368 data.slow_jobs.spawn("CHUNK_DROP", move || {
370 drop(chunks_to_remove);
371 });
372 }
373 }
374}
375
376#[derive(Debug)]
378pub struct NpcData {
379 pub pos: Pos,
380 pub stats: comp::Stats,
381 pub skill_set: comp::SkillSet,
382 pub health: Option<comp::Health>,
383 pub poise: comp::Poise,
384 pub inventory: comp::inventory::Inventory,
385 pub agent: Option<comp::Agent>,
386 pub body: comp::Body,
387 pub alignment: comp::Alignment,
388 pub scale: comp::Scale,
389 pub loot: LootSpec<String>,
390 pub pets: Vec<(NpcData, Vec3<f32>)>,
391 pub death_effects: Option<DeathEffects>,
392 pub rider_effects: Option<RiderEffects>,
393 pub rider: Option<Box<NpcData>>,
394}
395
396#[expect(clippy::large_enum_variant)] #[derive(Debug)]
402pub enum SpawnEntityData {
403 Npc(NpcData),
404 Special(Vec3<f32>, SpecialEntity),
405}
406
407impl SpawnEntityData {
408 pub fn from_entity_info(entity: EntityInfo) -> Self {
409 let EntityInfo {
410 special_entity,
412 has_agency,
413 agent_mark,
414 alignment,
415 no_flee,
416 idle_wander_factor,
417 aggro_range_multiplier,
418 body,
420 name,
421 scale,
422 pos,
423 loot,
424 skillset_asset,
426 loadout: mut loadout_builder,
427 inventory: items,
428 make_loadout,
429 trading_information: economy,
430 pets,
431 rider,
432 death_effects,
433 rider_effects,
434 } = entity;
435
436 if let Some(special) = special_entity {
437 return Self::Special(pos, special);
438 }
439
440 let name = name.unwrap_or_else(Content::dummy);
441 let stats = comp::Stats::new(name, body);
442
443 let skill_set = {
444 let skillset_builder = SkillSetBuilder::default();
445 if let Some(skillset_asset) = skillset_asset {
446 skillset_builder.with_asset_expect(&skillset_asset).build()
447 } else {
448 skillset_builder.build()
449 }
450 };
451
452 let inventory = {
453 if let Some(make_loadout) = make_loadout {
455 loadout_builder =
456 loadout_builder.with_creator(make_loadout, economy.as_ref(), None);
457 }
458 let loadout = loadout_builder.build();
459 let mut inventory = comp::inventory::Inventory::with_loadout(loadout, body);
460 for (num, mut item) in items {
461 if let Err(e) = item.set_amount(num) {
462 tracing::warn!(
463 "error during creating inventory for {name:?} at {pos}: {e:?}",
464 name = &stats.name,
465 );
466 }
467 if let Err(e) = inventory.push(item) {
468 tracing::warn!(
469 "error during creating inventory for {name:?} at {pos}: {e:?}",
470 name = &stats.name,
471 );
472 }
473 }
474
475 inventory
476 };
477
478 let health = Some(comp::Health::new(body));
479 let poise = comp::Poise::new(body);
480
481 let can_speak = match body {
483 comp::Body::Humanoid(_) => true,
484 comp::Body::BipedSmall(biped_small) => {
485 matches!(biped_small.species, biped_small::Species::Flamekeeper)
486 },
487 comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
488 bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
489 _ => false,
490 },
491 _ => false,
492 };
493
494 let trade_for_site = if matches!(agent_mark, Some(agent::Mark::Merchant)) {
495 economy.map(|e| e.id)
496 } else {
497 None
498 };
499
500 let agent = has_agency.then(|| {
501 let mut agent = comp::Agent::from_body(&body).with_behavior(
502 Behavior::default()
503 .maybe_with_capabilities(can_speak.then_some(BehaviorCapability::SPEAK))
504 .maybe_with_capabilities(trade_for_site.map(|_| BehaviorCapability::TRADE))
505 .with_trade_site(trade_for_site),
506 );
507
508 if !matches!(body, comp::Body::Humanoid(_)) {
510 agent = agent.with_patrol_origin(pos);
511 }
512
513 agent
514 .with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee)
515 .with_idle_wander_factor(idle_wander_factor)
516 .with_aggro_range_multiplier(aggro_range_multiplier)
517 });
518
519 let agent = if matches!(alignment, comp::Alignment::Enemy)
520 && matches!(body, comp::Body::Humanoid(_))
521 {
522 agent.map(|a| a.with_aggro_no_warn().with_no_flee_if(true))
523 } else {
524 agent
525 };
526
527 SpawnEntityData::Npc(NpcData {
528 pos: Pos(pos),
529 stats,
530 skill_set,
531 health,
532 poise,
533 inventory,
534 agent,
535 body,
536 alignment,
537 scale: comp::Scale(scale),
538 loot,
539 pets: {
540 let pet_count = pets.len() as f32;
541 pets.into_iter()
542 .enumerate()
543 .flat_map(|(i, pet)| {
544 Some((
545 SpawnEntityData::from_entity_info(pet)
546 .into_npc_data_inner()
547 .inspect_err(|data| {
548 error!("Pets must be SpawnEntityData::Npc, but found: {data:?}")
549 })
550 .ok()?,
551 Vec2::one()
552 .rotated_z(TAU * (i as f32 / pet_count))
553 .with_z(0.0)
554 * ((pet_count * 3.0) / TAU),
555 ))
556 })
557 .collect()
558 },
559 rider: rider.and_then(|e| {
560 Some(Box::new(
561 SpawnEntityData::from_entity_info(*e)
562 .into_npc_data_inner()
563 .ok()?,
564 ))
565 }),
566 death_effects,
567 rider_effects,
568 })
569 }
570
571 #[expect(clippy::result_large_err)]
572 pub fn into_npc_data_inner(self) -> Result<NpcData, Self> {
573 match self {
574 SpawnEntityData::Npc(inner) => Ok(inner),
575 other => Err(other),
576 }
577 }
578}
579
580impl NpcData {
581 pub fn to_npc_builder(self) -> (NpcBuilder, comp::Pos) {
582 let NpcData {
583 pos,
584 stats,
585 skill_set,
586 health,
587 poise,
588 inventory,
589 agent,
590 body,
591 alignment,
592 scale,
593 loot,
594 pets,
595 death_effects,
596 rider_effects,
597 rider,
598 } = self;
599
600 (
601 NpcBuilder::new(stats, body, alignment)
602 .with_skill_set(skill_set)
603 .with_health(health)
604 .with_poise(poise)
605 .with_inventory(inventory)
606 .with_agent(agent)
607 .with_scale(scale)
608 .with_loot(loot)
609 .with_pets(
610 pets.into_iter()
611 .map(|(pet, offset)| (pet.to_npc_builder().0, offset))
612 .collect::<Vec<_>>(),
613 )
614 .with_rider(rider.map(|rider| rider.to_npc_builder().0))
615 .with_death_effects(death_effects)
616 .with_rider_effects(rider_effects),
617 pos,
618 )
619 }
620}
621
622pub fn convert_to_loaded_vd(vd: u32, max_view_distance: u32) -> i32 {
623 const MAX_VD: u32 = 1 << 7;
634
635 const UNLOAD_THRESHOLD: u32 = 2;
638
639 (vd.clamp(crate::MIN_VD, max_view_distance)
641 .saturating_add(UNLOAD_THRESHOLD))
642 .min(MAX_VD) as i32
643}
644
645fn prepare_for_vd_check(
647 world_aabr_in_chunks: &Aabr<i32>,
648 max_view_distance: u32,
649 entity: Entity,
650 presence: &Presence,
651 pos: &Pos,
652 client: Option<u32>,
653) -> Option<((Vec2<i16>, i32), Entity, bool)> {
654 let is_client = client.is_some();
655 let pos = pos.0;
656 let vd = presence.terrain_view_distance.current();
657
658 let player_pos = pos.map(|x| x as i32);
661 let player_chunk_pos = TerrainGrid::chunk_key(player_pos);
662 let player_vd = convert_to_loaded_vd(vd, max_view_distance);
663
664 let player_aabr_in_chunks = Aabr {
675 min: player_chunk_pos - player_vd,
676 max: player_chunk_pos + player_vd,
677 };
678
679 (world_aabr_in_chunks.max.x >= player_aabr_in_chunks.min.x &&
680 world_aabr_in_chunks.min.x <= player_aabr_in_chunks.max.x &&
681 world_aabr_in_chunks.max.y >= player_aabr_in_chunks.min.y &&
682 world_aabr_in_chunks.min.y <= player_aabr_in_chunks.max.y)
683 .then(|| ((player_chunk_pos.as_::<i16>(), player_vd.pow(2)), entity, is_client))
691}
692
693pub fn prepare_player_presences<'a, P>(
694 world_size: Vec2<u32>,
695 max_view_distance: u32,
696 entities: &Entities<'a>,
697 positions: P,
698 presences: &ReadStorage<'a, Presence>,
699 clients: &ReadStorage<'a, Client>,
700) -> (Vec<((Vec2<i16>, i32), Entity)>, Vec<(Vec2<i16>, i32)>)
701where
702 P: GenericReadStorage<Component = Pos> + Join<Type = &'a Pos>,
703{
704 let world_aabr_in_chunks = Aabr {
708 min: Vec2::zero(),
709 max: world_size.map(|x| x.saturating_sub(1)).as_::<i32>(),
711 };
712
713 let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) =
714 (entities, presences, positions, clients.mask().maybe())
715 .join()
716 .filter_map(|(entity, presence, position, client)| {
717 prepare_for_vd_check(
718 &world_aabr_in_chunks,
719 max_view_distance,
720 entity,
721 presence,
722 position,
723 client,
724 )
725 })
726 .partition_map(|(player_data, entity, is_client)| {
727 if is_client {
731 Either::Left((player_data, entity))
732 } else {
733 Either::Right(player_data)
736 }
737 });
738
739 presences_positions_entities
745 .sort_unstable_by_key(|&((pos, vd2), _)| (pos.x, pos.y, Reverse(vd2)));
746 presences_positions.sort_unstable_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
747 presences_positions.extend(
753 presences_positions_entities
754 .iter()
755 .map(|&(player_data, _)| player_data),
756 );
757 presences_positions.sort_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
761 presences_positions.dedup_by_key(|&mut (pos, _)| pos);
770
771 (presences_positions_entities, presences_positions)
772}
773
774pub fn chunk_in_vd(player_chunk_pos: Vec2<i16>, player_vd_sqr: i32, chunk_pos: Vec2<i32>) -> bool {
775 let adjusted_dist_sqr = (player_chunk_pos.as_::<i32>() - chunk_pos).magnitude_squared();
778
779 adjusted_dist_sqr <= player_vd_sqr
780}