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::RepositionToFreeSpace, 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_entities: WriteStorage<'a, RepositionToFreeSpace>,
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_entities)
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, reposition.modify_waypoints))
260 })
261 .collect::<Vec<_>>();
262
263 for (entity, new_pos, modify_waypoints) in repositioned {
264 if modify_waypoints && let Some(waypoint) = data.waypoints.get_mut(entity) {
265 *waypoint = Waypoint::new(new_pos, *data.time);
266 }
267
268 data.reposition_entities.remove(entity);
269 }
270
271 let max_view_distance = data.server_settings.max_view_distance.unwrap_or(u32::MAX);
272 #[cfg(feature = "worldgen")]
273 let world_size = data.world.sim().get_size();
274 #[cfg(not(feature = "worldgen"))]
275 let world_size = data.world.map_size_lg().chunks().map(u32::from);
276 let (presences_position_entities, presences_positions) = prepare_player_presences(
277 world_size,
278 max_view_distance,
279 &data.entities,
280 &data.positions,
281 &data.presences,
282 &data.clients,
283 );
284 let real_max_view_distance = convert_to_loaded_vd(u32::MAX, max_view_distance);
285
286 new_chunks.par_iter().for_each_init(
288 || data.chunk_send_bus.emitter(),
289 |chunk_send_emitter, chunk_key| {
290 let min_chunk_x = chunk_key.x - real_max_view_distance;
298 let max_chunk_x = chunk_key.x + real_max_view_distance;
299 let start = presences_position_entities
300 .partition_point(|((pos, _), _)| i32::from(pos.x) < min_chunk_x);
301 let end = presences_position_entities
313 .partition_point(|((pos, _), _)| i32::from(pos.x) < max_chunk_x);
314 let interior = &presences_position_entities[start..end];
315 interior
316 .iter()
317 .filter(|((player_chunk_pos, player_vd_sqr), _)| {
318 chunk_in_vd(*player_chunk_pos, *player_vd_sqr, *chunk_key)
319 })
320 .for_each(|(_, entity)| {
321 chunk_send_emitter.emit(ChunkSendEntry {
322 entity: *entity,
323 chunk_key: *chunk_key,
324 });
325 });
326 },
327 );
328
329 let tick = (data.tick.0 % 16) as i32;
330
331 let chunks_to_remove = data.terrain
337 .par_keys()
338 .copied()
339 .chain(data.chunk_generator.par_pending_chunks())
342 .filter(|k| k.x % 4 + (k.y % 4) * 4 == tick)
353 .filter(|&chunk_key| {
354 let min_chunk_x = chunk_key.x - real_max_view_distance;
362 let max_chunk_x = chunk_key.x + real_max_view_distance;
363 let start = presences_positions
364 .partition_point(|(pos, _)| i32::from(pos.x) < min_chunk_x);
365 let end = presences_positions
375 .partition_point(|(pos, _)| i32::from(pos.x) < max_chunk_x);
376 let interior = &presences_positions[start..end];
377 !interior.iter().any(|&(player_chunk_pos, player_vd_sqr)| {
378 chunk_in_vd(player_chunk_pos, player_vd_sqr, chunk_key)
379 })
380 })
381 .collect::<Vec<_>>();
382
383 let chunks_to_remove = chunks_to_remove
384 .into_iter()
385 .filter_map(|key| {
386 #[cfg(feature = "persistent_world")]
388 if let Some(terrain_persistence) = data.terrain_persistence.as_mut() {
389 terrain_persistence.unload_chunk(key);
390 }
391
392 data.chunk_generator.cancel_if_pending(key);
393
394 data.terrain.remove(key).inspect(|_| {
400 data.terrain_changes.removed_chunks.insert(key);
401 })
402 })
403 .collect::<Vec<_>>();
404 if !chunks_to_remove.is_empty() {
405 data.slow_jobs.spawn("CHUNK_DROP", move || {
407 drop(chunks_to_remove);
408 });
409 }
410 }
411}
412
413#[derive(Debug)]
415pub struct NpcData {
416 pub pos: Pos,
417 pub stats: comp::Stats,
418 pub skill_set: comp::SkillSet,
419 pub health: Option<comp::Health>,
420 pub poise: comp::Poise,
421 pub inventory: comp::inventory::Inventory,
422 pub agent: Option<comp::Agent>,
423 pub body: comp::Body,
424 pub alignment: comp::Alignment,
425 pub scale: comp::Scale,
426 pub loot: LootSpec<String>,
427 pub pets: Vec<(NpcData, Vec3<f32>)>,
428 pub death_effects: Option<DeathEffects>,
429 pub rider_effects: Option<RiderEffects>,
430 pub rider: Option<Box<NpcData>>,
431}
432
433#[expect(clippy::large_enum_variant)] #[derive(Debug)]
439pub enum SpawnEntityData {
440 Npc(NpcData),
441 Special(Vec3<f32>, SpecialEntity),
442}
443
444impl SpawnEntityData {
445 pub fn from_entity_info(entity: EntityInfo) -> Self {
446 let EntityInfo {
447 special_entity,
449 has_agency,
450 agent_mark,
451 alignment,
452 no_flee,
453 idle_wander_factor,
454 aggro_range_multiplier,
455 body,
457 name,
458 scale,
459 pos,
460 loot,
461 skillset_asset,
463 loadout: mut loadout_builder,
464 inventory: items,
465 make_loadout,
466 trading_information: economy,
467 pets,
468 rider,
469 death_effects,
470 rider_effects,
471 } = entity;
472
473 if let Some(special) = special_entity {
474 return Self::Special(pos, special);
475 }
476
477 let name = name.unwrap_or_else(Content::dummy);
478 let stats = comp::Stats::new(name, body);
479
480 let skill_set = {
481 let skillset_builder = SkillSetBuilder::default();
482 if let Some(skillset_asset) = skillset_asset {
483 skillset_builder.with_asset_expect(&skillset_asset).build()
484 } else {
485 skillset_builder.build()
486 }
487 };
488
489 let inventory = {
490 if let Some(make_loadout) = make_loadout {
492 loadout_builder =
493 loadout_builder.with_creator(make_loadout, economy.as_ref(), None);
494 }
495 let loadout = loadout_builder.build();
496 let mut inventory = comp::inventory::Inventory::with_loadout(loadout, body);
497 for (num, mut item) in items {
498 if let Err(e) = item.set_amount(num) {
499 tracing::warn!(
500 "error during creating inventory for {name:?} at {pos}: {e:?}",
501 name = &stats.name,
502 );
503 }
504 if let Err(e) = inventory.push(item) {
505 tracing::warn!(
506 "error during creating inventory for {name:?} at {pos}: {e:?}",
507 name = &stats.name,
508 );
509 }
510 }
511
512 inventory
513 };
514
515 let health = Some(comp::Health::new(body));
516 let poise = comp::Poise::new(body);
517
518 let can_speak = match body {
520 comp::Body::Humanoid(_) => true,
521 comp::Body::BipedSmall(biped_small) => {
522 matches!(biped_small.species, biped_small::Species::Flamekeeper)
523 },
524 comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
525 bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
526 _ => false,
527 },
528 _ => false,
529 };
530
531 let trade_for_site = if matches!(agent_mark, Some(agent::Mark::Merchant)) {
532 economy.map(|e| e.id)
533 } else {
534 None
535 };
536
537 let agent = has_agency.then(|| {
538 let mut agent = comp::Agent::from_body(&body).with_behavior(
539 Behavior::default()
540 .maybe_with_capabilities(can_speak.then_some(BehaviorCapability::SPEAK))
541 .maybe_with_capabilities(trade_for_site.map(|_| BehaviorCapability::TRADE))
542 .with_trade_site(trade_for_site),
543 );
544
545 if !matches!(body, comp::Body::Humanoid(_)) {
547 agent = agent.with_patrol_origin(pos);
548 }
549
550 agent
551 .with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee)
552 .with_idle_wander_factor(idle_wander_factor)
553 .with_aggro_range_multiplier(aggro_range_multiplier)
554 });
555
556 let agent = if matches!(alignment, comp::Alignment::Enemy)
557 && matches!(body, comp::Body::Humanoid(_))
558 {
559 agent.map(|a| a.with_aggro_no_warn().with_no_flee_if(true))
560 } else {
561 agent
562 };
563
564 SpawnEntityData::Npc(NpcData {
565 pos: Pos(pos),
566 stats,
567 skill_set,
568 health,
569 poise,
570 inventory,
571 agent,
572 body,
573 alignment,
574 scale: comp::Scale(scale),
575 loot,
576 pets: {
577 let pet_count = pets.len() as f32;
578 pets.into_iter()
579 .enumerate()
580 .flat_map(|(i, pet)| {
581 Some((
582 SpawnEntityData::from_entity_info(pet)
583 .into_npc_data_inner()
584 .inspect_err(|data| {
585 error!("Pets must be SpawnEntityData::Npc, but found: {data:?}")
586 })
587 .ok()?,
588 Vec2::one()
589 .rotated_z(TAU * (i as f32 / pet_count))
590 .with_z(0.0)
591 * ((pet_count * 3.0) / TAU),
592 ))
593 })
594 .collect()
595 },
596 rider: rider.and_then(|e| {
597 Some(Box::new(
598 SpawnEntityData::from_entity_info(*e)
599 .into_npc_data_inner()
600 .ok()?,
601 ))
602 }),
603 death_effects,
604 rider_effects,
605 })
606 }
607
608 #[expect(clippy::result_large_err)]
609 pub fn into_npc_data_inner(self) -> Result<NpcData, Self> {
610 match self {
611 SpawnEntityData::Npc(inner) => Ok(inner),
612 other => Err(other),
613 }
614 }
615}
616
617impl NpcData {
618 pub fn to_npc_builder(self) -> (NpcBuilder, comp::Pos) {
619 let NpcData {
620 pos,
621 stats,
622 skill_set,
623 health,
624 poise,
625 inventory,
626 agent,
627 body,
628 alignment,
629 scale,
630 loot,
631 pets,
632 death_effects,
633 rider_effects,
634 rider,
635 } = self;
636
637 (
638 NpcBuilder::new(stats, body, alignment)
639 .with_skill_set(skill_set)
640 .with_health(health)
641 .with_poise(poise)
642 .with_inventory(inventory)
643 .with_agent(agent)
644 .with_scale(scale)
645 .with_loot(loot)
646 .with_pets(
647 pets.into_iter()
648 .map(|(pet, offset)| (pet.to_npc_builder().0, offset))
649 .collect::<Vec<_>>(),
650 )
651 .with_rider(rider.map(|rider| rider.to_npc_builder().0))
652 .with_death_effects(death_effects)
653 .with_rider_effects(rider_effects),
654 pos,
655 )
656 }
657}
658
659pub fn convert_to_loaded_vd(vd: u32, max_view_distance: u32) -> i32 {
660 const MAX_VD: u32 = 1 << 7;
671
672 const UNLOAD_THRESHOLD: u32 = 2;
675
676 (vd.clamp(crate::MIN_VD, max_view_distance)
678 .saturating_add(UNLOAD_THRESHOLD))
679 .min(MAX_VD) as i32
680}
681
682fn prepare_for_vd_check(
684 world_aabr_in_chunks: &Aabr<i32>,
685 max_view_distance: u32,
686 entity: Entity,
687 presence: &Presence,
688 pos: &Pos,
689 client: Option<u32>,
690) -> Option<((Vec2<i16>, i32), Entity, bool)> {
691 let is_client = client.is_some();
692 let pos = pos.0;
693 let vd = presence.terrain_view_distance.current();
694
695 let player_pos = pos.map(|x| x as i32);
698 let player_chunk_pos = TerrainGrid::chunk_key(player_pos);
699 let player_vd = convert_to_loaded_vd(vd, max_view_distance);
700
701 let player_aabr_in_chunks = Aabr {
712 min: player_chunk_pos - player_vd,
713 max: player_chunk_pos + player_vd,
714 };
715
716 (world_aabr_in_chunks.max.x >= player_aabr_in_chunks.min.x &&
717 world_aabr_in_chunks.min.x <= player_aabr_in_chunks.max.x &&
718 world_aabr_in_chunks.max.y >= player_aabr_in_chunks.min.y &&
719 world_aabr_in_chunks.min.y <= player_aabr_in_chunks.max.y)
720 .then(|| ((player_chunk_pos.as_::<i16>(), player_vd.pow(2)), entity, is_client))
728}
729
730pub fn prepare_player_presences<'a, P>(
731 world_size: Vec2<u32>,
732 max_view_distance: u32,
733 entities: &Entities<'a>,
734 positions: P,
735 presences: &ReadStorage<'a, Presence>,
736 clients: &ReadStorage<'a, Client>,
737) -> (Vec<((Vec2<i16>, i32), Entity)>, Vec<(Vec2<i16>, i32)>)
738where
739 P: GenericReadStorage<Component = Pos> + Join<Type = &'a Pos>,
740{
741 let world_aabr_in_chunks = Aabr {
745 min: Vec2::zero(),
746 max: world_size.map(|x| x.saturating_sub(1)).as_::<i32>(),
748 };
749
750 let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) =
751 (entities, presences, positions, clients.mask().maybe())
752 .join()
753 .filter_map(|(entity, presence, position, client)| {
754 prepare_for_vd_check(
755 &world_aabr_in_chunks,
756 max_view_distance,
757 entity,
758 presence,
759 position,
760 client,
761 )
762 })
763 .partition_map(|(player_data, entity, is_client)| {
764 if is_client {
768 Either::Left((player_data, entity))
769 } else {
770 Either::Right(player_data)
773 }
774 });
775
776 presences_positions_entities
782 .sort_unstable_by_key(|&((pos, vd2), _)| (pos.x, pos.y, Reverse(vd2)));
783 presences_positions.sort_unstable_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
784 presences_positions.extend(
790 presences_positions_entities
791 .iter()
792 .map(|&(player_data, _)| player_data),
793 );
794 presences_positions.sort_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
798 presences_positions.dedup_by_key(|&mut (pos, _)| pos);
807
808 (presences_positions_entities, presences_positions)
809}
810
811pub fn chunk_in_vd(player_chunk_pos: Vec2<i16>, player_vd_sqr: i32, chunk_pos: Vec2<i32>) -> bool {
812 let adjusted_dist_sqr = (player_chunk_pos.as_::<i32>() - chunk_pos).magnitude_squared();
815
816 adjusted_dist_sqr <= player_vd_sqr
817}