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, 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 });
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#[derive(Debug)]
401pub enum SpawnEntityData {
402 Npc(NpcData),
403 Special(Vec3<f32>, SpecialEntity),
404}
405
406impl SpawnEntityData {
407 pub fn from_entity_info(entity: EntityInfo) -> Self {
408 let EntityInfo {
409 special_entity,
411 has_agency,
412 agent_mark,
413 alignment,
414 no_flee,
415 idle_wander_factor,
416 aggro_range_multiplier,
417 body,
419 name,
420 scale,
421 pos,
422 loot,
423 skillset_asset,
425 loadout: mut loadout_builder,
426 inventory: items,
427 make_loadout,
428 trading_information: economy,
429 pets,
430 rider,
431 death_effects,
432 rider_effects,
433 } = entity;
434
435 if let Some(special) = special_entity {
436 return Self::Special(pos, special);
437 }
438
439 let name = name.unwrap_or_else(|| "Unnamed".to_string());
440 let stats = comp::Stats::new(name, body);
441
442 let skill_set = {
443 let skillset_builder = SkillSetBuilder::default();
444 if let Some(skillset_asset) = skillset_asset {
445 skillset_builder.with_asset_expect(&skillset_asset).build()
446 } else {
447 skillset_builder.build()
448 }
449 };
450
451 let inventory = {
452 if let Some(make_loadout) = make_loadout {
454 loadout_builder =
455 loadout_builder.with_creator(make_loadout, economy.as_ref(), None);
456 }
457 let loadout = loadout_builder.build();
458 let mut inventory = comp::inventory::Inventory::with_loadout(loadout, body);
459 for (num, mut item) in items {
460 if let Err(e) = item.set_amount(num) {
461 tracing::warn!(
462 "error during creating inventory for {name} at {pos}: {e:?}",
463 name = &stats.name,
464 );
465 }
466 if let Err(e) = inventory.push(item) {
467 tracing::warn!(
468 "error during creating inventory for {name} at {pos}: {e:?}",
469 name = &stats.name,
470 );
471 }
472 }
473
474 inventory
475 };
476
477 let health = Some(comp::Health::new(body));
478 let poise = comp::Poise::new(body);
479
480 let can_speak = match body {
482 comp::Body::Humanoid(_) => true,
483 comp::Body::BipedSmall(biped_small) => {
484 matches!(biped_small.species, biped_small::Species::Flamekeeper)
485 },
486 comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
487 bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
488 _ => false,
489 },
490 _ => false,
491 };
492
493 let trade_for_site = if matches!(agent_mark, Some(agent::Mark::Merchant)) {
494 economy.map(|e| e.id)
495 } else {
496 None
497 };
498
499 let agent = has_agency.then(|| {
500 let mut agent = comp::Agent::from_body(&body).with_behavior(
501 Behavior::default()
502 .maybe_with_capabilities(can_speak.then_some(BehaviorCapability::SPEAK))
503 .maybe_with_capabilities(trade_for_site.map(|_| BehaviorCapability::TRADE))
504 .with_trade_site(trade_for_site),
505 );
506
507 if !matches!(body, comp::Body::Humanoid(_)) {
509 agent = agent.with_patrol_origin(pos);
510 }
511
512 agent
513 .with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee)
514 .with_idle_wander_factor(idle_wander_factor)
515 .with_aggro_range_multiplier(aggro_range_multiplier)
516 });
517
518 let agent = if matches!(alignment, comp::Alignment::Enemy)
519 && matches!(body, comp::Body::Humanoid(_))
520 {
521 agent.map(|a| a.with_aggro_no_warn().with_no_flee_if(true))
522 } else {
523 agent
524 };
525
526 SpawnEntityData::Npc(NpcData {
527 pos: Pos(pos),
528 stats,
529 skill_set,
530 health,
531 poise,
532 inventory,
533 agent,
534 body,
535 alignment,
536 scale: comp::Scale(scale),
537 loot,
538 pets: {
539 let pet_count = pets.len() as f32;
540 pets.into_iter()
541 .enumerate()
542 .flat_map(|(i, pet)| {
543 Some((
544 SpawnEntityData::from_entity_info(pet)
545 .into_npc_data_inner()
546 .inspect_err(|data| {
547 error!("Pets must be SpawnEntityData::Npc, but found: {data:?}")
548 })
549 .ok()?,
550 Vec2::one()
551 .rotated_z(TAU * (i as f32 / pet_count))
552 .with_z(0.0)
553 * ((pet_count * 3.0) / TAU),
554 ))
555 })
556 .collect()
557 },
558 rider: rider.and_then(|e| {
559 Some(Box::new(
560 SpawnEntityData::from_entity_info(*e)
561 .into_npc_data_inner()
562 .ok()?,
563 ))
564 }),
565 death_effects,
566 rider_effects,
567 })
568 }
569
570 pub fn into_npc_data_inner(self) -> Result<NpcData, Self> {
571 match self {
572 SpawnEntityData::Npc(inner) => Ok(inner),
573 other => Err(other),
574 }
575 }
576}
577
578impl NpcData {
579 pub fn to_npc_builder(self) -> (NpcBuilder, comp::Pos) {
580 let NpcData {
581 pos,
582 stats,
583 skill_set,
584 health,
585 poise,
586 inventory,
587 agent,
588 body,
589 alignment,
590 scale,
591 loot,
592 pets,
593 death_effects,
594 rider_effects,
595 rider,
596 } = self;
597
598 (
599 NpcBuilder::new(stats, body, alignment)
600 .with_skill_set(skill_set)
601 .with_health(health)
602 .with_poise(poise)
603 .with_inventory(inventory)
604 .with_agent(agent)
605 .with_scale(scale)
606 .with_loot(loot)
607 .with_pets(
608 pets.into_iter()
609 .map(|(pet, offset)| (pet.to_npc_builder().0, offset))
610 .collect::<Vec<_>>(),
611 )
612 .with_rider(rider.map(|rider| rider.to_npc_builder().0))
613 .with_death_effects(death_effects)
614 .with_rider_effects(rider_effects),
615 pos,
616 )
617 }
618}
619
620pub fn convert_to_loaded_vd(vd: u32, max_view_distance: u32) -> i32 {
621 const MAX_VD: u32 = 1 << 7;
632
633 const UNLOAD_THRESHOLD: u32 = 2;
636
637 (vd.clamp(crate::MIN_VD, max_view_distance)
639 .saturating_add(UNLOAD_THRESHOLD))
640 .min(MAX_VD) as i32
641}
642
643fn prepare_for_vd_check(
645 world_aabr_in_chunks: &Aabr<i32>,
646 max_view_distance: u32,
647 entity: Entity,
648 presence: &Presence,
649 pos: &Pos,
650 client: Option<u32>,
651) -> Option<((Vec2<i16>, i32), Entity, bool)> {
652 let is_client = client.is_some();
653 let pos = pos.0;
654 let vd = presence.terrain_view_distance.current();
655
656 let player_pos = pos.map(|x| x as i32);
659 let player_chunk_pos = TerrainGrid::chunk_key(player_pos);
660 let player_vd = convert_to_loaded_vd(vd, max_view_distance);
661
662 let player_aabr_in_chunks = Aabr {
673 min: player_chunk_pos - player_vd,
674 max: player_chunk_pos + player_vd,
675 };
676
677 (world_aabr_in_chunks.max.x >= player_aabr_in_chunks.min.x &&
678 world_aabr_in_chunks.min.x <= player_aabr_in_chunks.max.x &&
679 world_aabr_in_chunks.max.y >= player_aabr_in_chunks.min.y &&
680 world_aabr_in_chunks.min.y <= player_aabr_in_chunks.max.y)
681 .then(|| ((player_chunk_pos.as_::<i16>(), player_vd.pow(2)), entity, is_client))
689}
690
691pub fn prepare_player_presences<'a, P>(
692 world_size: Vec2<u32>,
693 max_view_distance: u32,
694 entities: &Entities<'a>,
695 positions: P,
696 presences: &ReadStorage<'a, Presence>,
697 clients: &ReadStorage<'a, Client>,
698) -> (Vec<((Vec2<i16>, i32), Entity)>, Vec<(Vec2<i16>, i32)>)
699where
700 P: GenericReadStorage<Component = Pos> + Join<Type = &'a Pos>,
701{
702 let world_aabr_in_chunks = Aabr {
706 min: Vec2::zero(),
707 max: world_size.map(|x| x.saturating_sub(1)).as_::<i32>(),
709 };
710
711 let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) =
712 (entities, presences, positions, clients.mask().maybe())
713 .join()
714 .filter_map(|(entity, presence, position, client)| {
715 prepare_for_vd_check(
716 &world_aabr_in_chunks,
717 max_view_distance,
718 entity,
719 presence,
720 position,
721 client,
722 )
723 })
724 .partition_map(|(player_data, entity, is_client)| {
725 if is_client {
729 Either::Left((player_data, entity))
730 } else {
731 Either::Right(player_data)
734 }
735 });
736
737 presences_positions_entities
743 .sort_unstable_by_key(|&((pos, vd2), _)| (pos.x, pos.y, Reverse(vd2)));
744 presences_positions.sort_unstable_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
745 presences_positions.extend(
751 presences_positions_entities
752 .iter()
753 .map(|&(player_data, _)| player_data),
754 );
755 presences_positions.sort_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
759 presences_positions.dedup_by_key(|&mut (pos, _)| pos);
768
769 (presences_positions_entities, presences_positions)
770}
771
772pub fn chunk_in_vd(player_chunk_pos: Vec2<i16>, player_vd_sqr: i32, chunk_pos: Vec2<i32>) -> bool {
773 let adjusted_dist_sqr = (player_chunk_pos.as_::<i32>() - chunk_pos).magnitude_squared();
776
777 adjusted_dist_sqr <= player_vd_sqr
778}