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::{
23 CreateNpcEvent, CreateNpcGroupEvent, CreateSpecialEntityEvent, EmitExt, EventBus,
24 NpcBuilder,
25 },
26 event_emitters,
27 generation::{EntityInfo, EntitySpawn, SpecialEntity},
28 lottery::LootSpec,
29 resources::{Time, TimeOfDay},
30 slowjob::SlowJobPool,
31 terrain::TerrainGrid,
32 util::Dir,
33};
34
35use common_ecs::{Job, Origin, Phase, System};
36use common_net::msg::ServerGeneral;
37use common_state::TerrainChanges;
38use comp::Behavior;
39use core::cmp::Reverse;
40use itertools::Itertools;
41use rayon::{iter::Either, prelude::*};
42use specs::{
43 Entities, Entity, Join, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write,
44 WriteExpect, WriteStorage, shred, storage::GenericReadStorage,
45};
46use std::{f32::consts::TAU, sync::Arc};
47use vek::*;
48
49#[cfg(feature = "persistent_world")]
50pub type TerrainPersistenceData<'a> = Option<Write<'a, TerrainPersistence>>;
51#[cfg(not(feature = "persistent_world"))]
52pub type TerrainPersistenceData<'a> = ();
53
54pub const SAFE_ZONE_RADIUS: f32 = 200.0;
55
56#[cfg(feature = "worldgen")]
57type RtSimData<'a> = WriteExpect<'a, rtsim::RtSim>;
58#[cfg(not(feature = "worldgen"))]
59type RtSimData<'a> = ();
60
61event_emitters! {
62 struct Events[Emitters] {
63 create_npc: CreateNpcEvent,
64 create_npc_group: CreateNpcGroupEvent,
65 create_waypoint: CreateSpecialEntityEvent,
66 }
67}
68
69#[derive(SystemData)]
70pub struct Data<'a> {
71 events: Events<'a>,
72 tick: Read<'a, Tick>,
73 server_settings: Read<'a, Settings>,
74 time_of_day: Read<'a, TimeOfDay>,
75 calendar: Read<'a, Calendar>,
76 slow_jobs: ReadExpect<'a, SlowJobPool>,
77 index: ReadExpect<'a, IndexOwned>,
78 world: ReadExpect<'a, Arc<World>>,
79 chunk_send_bus: ReadExpect<'a, EventBus<ChunkSendEntry>>,
80 chunk_generator: WriteExpect<'a, ChunkGenerator>,
81 terrain: WriteExpect<'a, TerrainGrid>,
82 terrain_changes: Write<'a, TerrainChanges>,
83 chunk_requests: Write<'a, Vec<ChunkRequest>>,
84 rtsim: RtSimData<'a>,
85 #[cfg(feature = "persistent_world")]
86 terrain_persistence: TerrainPersistenceData<'a>,
87 positions: WriteStorage<'a, Pos>,
88 presences: ReadStorage<'a, Presence>,
89 clients: ReadStorage<'a, Client>,
90 entities: Entities<'a>,
91 reposition_on_load: WriteStorage<'a, RepositionOnChunkLoad>,
92 forced_updates: WriteStorage<'a, ForceUpdate>,
93 waypoints: WriteStorage<'a, Waypoint>,
94 time: ReadExpect<'a, Time>,
95}
96
97#[derive(Default)]
104pub struct Sys;
105impl<'a> System<'a> for Sys {
106 type SystemData = Data<'a>;
107
108 const NAME: &'static str = "terrain";
109 const ORIGIN: Origin = Origin::Server;
110 const PHASE: Phase = Phase::Create;
111
112 fn run(_job: &mut Job<Self>, mut data: Self::SystemData) {
113 let mut emitters = data.events.get_emitters();
114
115 data.chunk_requests.drain(..).for_each(|request| {
121 data.chunk_generator.generate_chunk(
122 Some(request.entity),
123 request.key,
124 &data.slow_jobs,
125 Arc::clone(&data.world),
126 &data.rtsim,
127 data.index.clone(),
128 (*data.time_of_day, data.calendar.clone()),
129 )
130 });
131
132 let mut rng = rand::rng();
133 let mut new_chunks = Vec::new();
136 'insert_terrain_chunks: while let Some((key, res)) = data.chunk_generator.recv_new_chunk() {
137 #[cfg_attr(not(feature = "persistent_world"), expect(unused_mut))]
138 let (mut chunk, supplement) = match res {
139 Ok((chunk, supplement)) => (chunk, supplement),
140 Err(Some(entity)) => {
141 if let Some(client) = data.clients.get(entity) {
142 client.send_fallible(ServerGeneral::TerrainChunkUpdate {
143 key,
144 chunk: Err(()),
145 });
146 }
147 continue 'insert_terrain_chunks;
148 },
149 Err(None) => {
150 continue 'insert_terrain_chunks;
151 },
152 };
153
154 #[cfg(feature = "persistent_world")]
156 if let Some(terrain_persistence) = data.terrain_persistence.as_mut() {
157 terrain_persistence.apply_changes(key, &mut chunk);
158 }
159
160 let chunk = Arc::new(chunk);
162
163 new_chunks.push(key);
165
166 if data.terrain.insert(key, chunk).is_some() {
169 data.terrain_changes.modified_chunks.insert(key);
170 } else {
171 data.terrain_changes.new_chunks.insert(key);
172 #[cfg(feature = "worldgen")]
173 data.rtsim
174 .hook_load_chunk(key, supplement.rtsim_max_resources, &data.world);
175 }
176
177 for entity_spawn in supplement.entity_spawns {
179 let check_pos = |pos: Vec3<f32>| {
181 assert!(
182 data.terrain
183 .pos_key(pos.map(|e| e.floor() as i32))
184 .map2(key, |e, tgt| (e - tgt).abs() <= 1)
185 .reduce_and(),
186 "Chunk spawned entity that wasn't nearby",
187 )
188 };
189
190 match entity_spawn {
191 EntitySpawn::Entity(entity) => {
192 check_pos(entity.pos);
193
194 let data = SpawnEntityData::from_entity_info(*entity);
195 match data {
196 SpawnEntityData::Special(pos, entity) => {
197 emitters.emit(CreateSpecialEntityEvent { pos, entity });
198 },
199 SpawnEntityData::Npc(data) => {
200 let (npc_builder, pos) = data.to_npc_builder();
201
202 emitters.emit(CreateNpcEvent {
203 pos,
204 ori: comp::Ori::from(Dir::random_2d(&mut rng)),
205 npc: npc_builder.with_anchor(comp::Anchor::Chunk(key)),
206 });
207 },
208 }
209 },
210 EntitySpawn::Group(group) => {
211 for entity in group.iter() {
212 check_pos(entity.pos);
213 }
214
215 let create_npc_events = group
216 .into_iter()
217 .filter_map(|entity| match SpawnEntityData::from_entity_info(entity) {
218 SpawnEntityData::Special(..) => None,
219 SpawnEntityData::Npc(data) => {
220 let (npc_builder, pos) = data.to_npc_builder();
221 Some(CreateNpcEvent {
222 pos,
223 ori: comp::Ori::from(Dir::random_2d(&mut rng)),
224 npc: npc_builder.with_anchor(comp::Anchor::Chunk(key)),
225 })
226 },
227 })
228 .collect::<Vec<_>>();
229
230 emitters.emit(CreateNpcGroupEvent {
231 npcs: create_npc_events,
232 });
233 },
234 }
235 }
236 }
237
238 let repositioned = (&data.entities, &mut data.positions, (&mut data.forced_updates).maybe(), &data.reposition_on_load)
241 .par_join()
244 .filter_map(|(entity, pos, force_update, reposition)| {
245 let entity_pos = pos.0.map(|x| x as i32);
248 let chunk_pos = TerrainGrid::chunk_key(entity_pos);
251 let chunk = data.terrain.get_key(chunk_pos)?;
252 let new_pos = if reposition.needs_ground {
253 data.terrain.try_find_ground(entity_pos)
254 } else {
255 data.terrain.try_find_space(entity_pos)
256 }.map(|x| x.as_::<f32>()).unwrap_or_else(|| chunk.find_accessible_pos(entity_pos.xy(), false));
257 pos.0 = new_pos;
258 force_update.map(|force_update| force_update.update());
259 Some((entity, new_pos))
260 })
261 .collect::<Vec<_>>();
262
263 for (entity, new_pos) in repositioned {
264 if let Some(waypoint) = data.waypoints.get_mut(entity) {
265 *waypoint = Waypoint::new(new_pos, *data.time);
266 }
267 data.reposition_on_load.remove(entity);
268 }
269
270 let max_view_distance = data.server_settings.max_view_distance.unwrap_or(u32::MAX);
271 #[cfg(feature = "worldgen")]
272 let world_size = data.world.sim().get_size();
273 #[cfg(not(feature = "worldgen"))]
274 let world_size = data.world.map_size_lg().chunks().map(u32::from);
275 let (presences_position_entities, presences_positions) = prepare_player_presences(
276 world_size,
277 max_view_distance,
278 &data.entities,
279 &data.positions,
280 &data.presences,
281 &data.clients,
282 );
283 let real_max_view_distance = convert_to_loaded_vd(u32::MAX, max_view_distance);
284
285 new_chunks.par_iter().for_each_init(
287 || data.chunk_send_bus.emitter(),
288 |chunk_send_emitter, chunk_key| {
289 let min_chunk_x = chunk_key.x - real_max_view_distance;
297 let max_chunk_x = chunk_key.x + real_max_view_distance;
298 let start = presences_position_entities
299 .partition_point(|((pos, _), _)| i32::from(pos.x) < min_chunk_x);
300 let end = presences_position_entities
312 .partition_point(|((pos, _), _)| i32::from(pos.x) < max_chunk_x);
313 let interior = &presences_position_entities[start..end];
314 interior
315 .iter()
316 .filter(|((player_chunk_pos, player_vd_sqr), _)| {
317 chunk_in_vd(*player_chunk_pos, *player_vd_sqr, *chunk_key)
318 })
319 .for_each(|(_, entity)| {
320 chunk_send_emitter.emit(ChunkSendEntry {
321 entity: *entity,
322 chunk_key: *chunk_key,
323 });
324 });
325 },
326 );
327
328 let tick = (data.tick.0 % 16) as i32;
329
330 let chunks_to_remove = data.terrain
336 .par_keys()
337 .copied()
338 .chain(data.chunk_generator.par_pending_chunks())
341 .filter(|k| k.x % 4 + (k.y % 4) * 4 == tick)
352 .filter(|&chunk_key| {
353 let min_chunk_x = chunk_key.x - real_max_view_distance;
361 let max_chunk_x = chunk_key.x + real_max_view_distance;
362 let start = presences_positions
363 .partition_point(|(pos, _)| i32::from(pos.x) < min_chunk_x);
364 let end = presences_positions
374 .partition_point(|(pos, _)| i32::from(pos.x) < max_chunk_x);
375 let interior = &presences_positions[start..end];
376 !interior.iter().any(|&(player_chunk_pos, player_vd_sqr)| {
377 chunk_in_vd(player_chunk_pos, player_vd_sqr, chunk_key)
378 })
379 })
380 .collect::<Vec<_>>();
381
382 let chunks_to_remove = chunks_to_remove
383 .into_iter()
384 .filter_map(|key| {
385 #[cfg(feature = "persistent_world")]
387 if let Some(terrain_persistence) = data.terrain_persistence.as_mut() {
388 terrain_persistence.unload_chunk(key);
389 }
390
391 data.chunk_generator.cancel_if_pending(key);
392
393 data.terrain.remove(key).inspect(|_| {
399 data.terrain_changes.removed_chunks.insert(key);
400 })
401 })
402 .collect::<Vec<_>>();
403 if !chunks_to_remove.is_empty() {
404 data.slow_jobs.spawn("CHUNK_DROP", move || {
406 drop(chunks_to_remove);
407 });
408 }
409 }
410}
411
412#[derive(Debug)]
414pub struct NpcData {
415 pub pos: Pos,
416 pub stats: comp::Stats,
417 pub skill_set: comp::SkillSet,
418 pub health: Option<comp::Health>,
419 pub poise: comp::Poise,
420 pub inventory: comp::inventory::Inventory,
421 pub agent: Option<comp::Agent>,
422 pub body: comp::Body,
423 pub alignment: comp::Alignment,
424 pub scale: comp::Scale,
425 pub loot: LootSpec<String>,
426 pub pets: Vec<(NpcData, Vec3<f32>)>,
427 pub death_effects: Option<DeathEffects>,
428 pub rider_effects: Option<RiderEffects>,
429 pub rider: Option<Box<NpcData>>,
430}
431
432#[expect(clippy::large_enum_variant)] #[derive(Debug)]
438pub enum SpawnEntityData {
439 Npc(NpcData),
440 Special(Vec3<f32>, SpecialEntity),
441}
442
443impl SpawnEntityData {
444 pub fn from_entity_info(entity: EntityInfo) -> Self {
445 let EntityInfo {
446 special_entity,
448 has_agency,
449 agent_mark,
450 alignment,
451 no_flee,
452 idle_wander_factor,
453 aggro_range_multiplier,
454 body,
456 name,
457 scale,
458 pos,
459 loot,
460 skillset_asset,
462 loadout: mut loadout_builder,
463 inventory: items,
464 make_loadout,
465 trading_information: economy,
466 pets,
467 rider,
468 death_effects,
469 rider_effects,
470 } = entity;
471
472 if let Some(special) = special_entity {
473 return Self::Special(pos, special);
474 }
475
476 let name = name.unwrap_or_else(Content::dummy);
477 let stats = comp::Stats::new(name, body);
478
479 let skill_set = {
480 let skillset_builder = SkillSetBuilder::default();
481 if let Some(skillset_asset) = skillset_asset {
482 skillset_builder.with_asset_expect(&skillset_asset).build()
483 } else {
484 skillset_builder.build()
485 }
486 };
487
488 let inventory = {
489 if let Some(make_loadout) = make_loadout {
491 loadout_builder =
492 loadout_builder.with_creator(make_loadout, economy.as_ref(), None);
493 }
494 let loadout = loadout_builder.build();
495 let mut inventory = comp::inventory::Inventory::with_loadout(loadout, body);
496 for (num, mut item) in items {
497 if let Err(e) = item.set_amount(num) {
498 tracing::warn!(
499 "error during creating inventory for {name:?} at {pos}: {e:?}",
500 name = &stats.name,
501 );
502 }
503 if let Err(e) = inventory.push(item) {
504 tracing::warn!(
505 "error during creating inventory for {name:?} at {pos}: {e:?}",
506 name = &stats.name,
507 );
508 }
509 }
510
511 inventory
512 };
513
514 let health = Some(comp::Health::new(body));
515 let poise = comp::Poise::new(body);
516
517 let can_speak = match body {
519 comp::Body::Humanoid(_) => true,
520 comp::Body::BipedSmall(biped_small) => {
521 matches!(biped_small.species, biped_small::Species::Flamekeeper)
522 },
523 comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
524 bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
525 _ => false,
526 },
527 _ => false,
528 };
529
530 let trade_for_site = if matches!(agent_mark, Some(agent::Mark::Merchant)) {
531 economy.map(|e| e.id)
532 } else {
533 None
534 };
535
536 let agent = has_agency.then(|| {
537 let mut agent = comp::Agent::from_body(&body).with_behavior(
538 Behavior::default()
539 .maybe_with_capabilities(can_speak.then_some(BehaviorCapability::SPEAK))
540 .maybe_with_capabilities(trade_for_site.map(|_| BehaviorCapability::TRADE))
541 .with_trade_site(trade_for_site),
542 );
543
544 if !matches!(body, comp::Body::Humanoid(_)) {
546 agent = agent.with_patrol_origin(pos);
547 }
548
549 agent
550 .with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee)
551 .with_idle_wander_factor(idle_wander_factor)
552 .with_aggro_range_multiplier(aggro_range_multiplier)
553 });
554
555 let agent = if matches!(alignment, comp::Alignment::Enemy)
556 && matches!(body, comp::Body::Humanoid(_))
557 {
558 agent.map(|a| a.with_aggro_no_warn().with_no_flee_if(true))
559 } else {
560 agent
561 };
562
563 SpawnEntityData::Npc(NpcData {
564 pos: Pos(pos),
565 stats,
566 skill_set,
567 health,
568 poise,
569 inventory,
570 agent,
571 body,
572 alignment,
573 scale: comp::Scale(scale),
574 loot,
575 pets: {
576 let pet_count = pets.len() as f32;
577 pets.into_iter()
578 .enumerate()
579 .flat_map(|(i, pet)| {
580 Some((
581 SpawnEntityData::from_entity_info(pet)
582 .into_npc_data_inner()
583 .inspect_err(|data| {
584 error!("Pets must be SpawnEntityData::Npc, but found: {data:?}")
585 })
586 .ok()?,
587 Vec2::one()
588 .rotated_z(TAU * (i as f32 / pet_count))
589 .with_z(0.0)
590 * ((pet_count * 3.0) / TAU),
591 ))
592 })
593 .collect()
594 },
595 rider: rider.and_then(|e| {
596 Some(Box::new(
597 SpawnEntityData::from_entity_info(*e)
598 .into_npc_data_inner()
599 .ok()?,
600 ))
601 }),
602 death_effects,
603 rider_effects,
604 })
605 }
606
607 #[expect(clippy::result_large_err)]
608 pub fn into_npc_data_inner(self) -> Result<NpcData, Self> {
609 match self {
610 SpawnEntityData::Npc(inner) => Ok(inner),
611 other => Err(other),
612 }
613 }
614}
615
616impl NpcData {
617 pub fn to_npc_builder(self) -> (NpcBuilder, comp::Pos) {
618 let NpcData {
619 pos,
620 stats,
621 skill_set,
622 health,
623 poise,
624 inventory,
625 agent,
626 body,
627 alignment,
628 scale,
629 loot,
630 pets,
631 death_effects,
632 rider_effects,
633 rider,
634 } = self;
635
636 (
637 NpcBuilder::new(stats, body, alignment)
638 .with_skill_set(skill_set)
639 .with_health(health)
640 .with_poise(poise)
641 .with_inventory(inventory)
642 .with_agent(agent)
643 .with_scale(scale)
644 .with_loot(loot)
645 .with_pets(
646 pets.into_iter()
647 .map(|(pet, offset)| (pet.to_npc_builder().0, offset))
648 .collect::<Vec<_>>(),
649 )
650 .with_rider(rider.map(|rider| rider.to_npc_builder().0))
651 .with_death_effects(death_effects)
652 .with_rider_effects(rider_effects),
653 pos,
654 )
655 }
656}
657
658pub fn convert_to_loaded_vd(vd: u32, max_view_distance: u32) -> i32 {
659 const MAX_VD: u32 = 1 << 7;
670
671 const UNLOAD_THRESHOLD: u32 = 2;
674
675 (vd.clamp(crate::MIN_VD, max_view_distance)
677 .saturating_add(UNLOAD_THRESHOLD))
678 .min(MAX_VD) as i32
679}
680
681fn prepare_for_vd_check(
683 world_aabr_in_chunks: &Aabr<i32>,
684 max_view_distance: u32,
685 entity: Entity,
686 presence: &Presence,
687 pos: &Pos,
688 client: Option<u32>,
689) -> Option<((Vec2<i16>, i32), Entity, bool)> {
690 let is_client = client.is_some();
691 let pos = pos.0;
692 let vd = presence.terrain_view_distance.current();
693
694 let player_pos = pos.map(|x| x as i32);
697 let player_chunk_pos = TerrainGrid::chunk_key(player_pos);
698 let player_vd = convert_to_loaded_vd(vd, max_view_distance);
699
700 let player_aabr_in_chunks = Aabr {
711 min: player_chunk_pos - player_vd,
712 max: player_chunk_pos + player_vd,
713 };
714
715 (world_aabr_in_chunks.max.x >= player_aabr_in_chunks.min.x &&
716 world_aabr_in_chunks.min.x <= player_aabr_in_chunks.max.x &&
717 world_aabr_in_chunks.max.y >= player_aabr_in_chunks.min.y &&
718 world_aabr_in_chunks.min.y <= player_aabr_in_chunks.max.y)
719 .then(|| ((player_chunk_pos.as_::<i16>(), player_vd.pow(2)), entity, is_client))
727}
728
729pub fn prepare_player_presences<'a, P>(
730 world_size: Vec2<u32>,
731 max_view_distance: u32,
732 entities: &Entities<'a>,
733 positions: P,
734 presences: &ReadStorage<'a, Presence>,
735 clients: &ReadStorage<'a, Client>,
736) -> (Vec<((Vec2<i16>, i32), Entity)>, Vec<(Vec2<i16>, i32)>)
737where
738 P: GenericReadStorage<Component = Pos> + Join<Type = &'a Pos>,
739{
740 let world_aabr_in_chunks = Aabr {
744 min: Vec2::zero(),
745 max: world_size.map(|x| x.saturating_sub(1)).as_::<i32>(),
747 };
748
749 let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) =
750 (entities, presences, positions, clients.mask().maybe())
751 .join()
752 .filter_map(|(entity, presence, position, client)| {
753 prepare_for_vd_check(
754 &world_aabr_in_chunks,
755 max_view_distance,
756 entity,
757 presence,
758 position,
759 client,
760 )
761 })
762 .partition_map(|(player_data, entity, is_client)| {
763 if is_client {
767 Either::Left((player_data, entity))
768 } else {
769 Either::Right(player_data)
772 }
773 });
774
775 presences_positions_entities
781 .sort_unstable_by_key(|&((pos, vd2), _)| (pos.x, pos.y, Reverse(vd2)));
782 presences_positions.sort_unstable_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
783 presences_positions.extend(
789 presences_positions_entities
790 .iter()
791 .map(|&(player_data, _)| player_data),
792 );
793 presences_positions.sort_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
797 presences_positions.dedup_by_key(|&mut (pos, _)| pos);
806
807 (presences_positions_entities, presences_positions)
808}
809
810pub fn chunk_in_vd(player_chunk_pos: Vec2<i16>, player_vd_sqr: i32, chunk_pos: Vec2<i32>) -> bool {
811 let adjusted_dist_sqr = (player_chunk_pos.as_::<i32>() - chunk_pos).magnitude_squared();
814
815 adjusted_dist_sqr <= player_vd_sqr
816}