veloren_server/sys/
terrain.rs

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/// This system will handle loading generated chunks and unloading
98/// unneeded chunks.
99///     1. Inserts newly generated chunks into the TerrainGrid
100///     2. Sends new chunks to nearby clients
101///     3. Handles the chunk's supplement (e.g. npcs)
102///     4. Removes chunks outside the range of players
103#[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        // Generate requested chunks
116        //
117        // Submit requests for chunks right before receiving finished chunks so that we
118        // don't create duplicate work for chunks that just finished but are not
119        // yet added to the terrain.
120        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        // Fetch any generated `TerrainChunk`s and insert them into the terrain.
134        // Also, send the chunk data to anybody that is close by.
135        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            // Apply changes from terrain persistence to this chunk
155            #[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            // Arcify the chunk
161            let chunk = Arc::new(chunk);
162
163            // Add to list of chunks to send to nearby players.
164            new_chunks.push(key);
165
166            // TODO: code duplication for chunk insertion between here and state.rs
167            // Insert the chunk into terrain changes
168            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            // Handle chunk supplement
178            for entity_spawn in supplement.entity_spawns {
179                // Check this because it's a common source of weird bugs
180                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        // TODO: Consider putting this in another system since this forces us to take
239        // positions by write rather than read access.
240        let repositioned = (&data.entities, &mut data.positions, (&mut data.forced_updates).maybe(), &data.reposition_on_load)
241            // TODO: Consider using par_bridge() because Rayon has very poor work splitting for
242            // sparse joins.
243            .par_join()
244            .filter_map(|(entity, pos, force_update, reposition)| {
245                // NOTE: We use regular as casts rather than as_ because we want to saturate on
246                // overflow.
247                let entity_pos = pos.0.map(|x| x as i32);
248                // If an entity is marked as needing repositioning once the chunk loads (e.g.
249                // from having just logged in), reposition them.
250                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        // Send the chunks to all nearby players.
286        new_chunks.par_iter().for_each_init(
287            || data.chunk_send_bus.emitter(),
288            |chunk_send_emitter, chunk_key| {
289                // We only have to check players inside the maximum view distance of the server
290                // of our own position.
291                //
292                // We start by partitioning by X, finding only entities in chunks within the X
293                // range of us.  These are guaranteed in bounds due to restrictions on max view
294                // distance (namely: the square of any chunk coordinate plus the max view
295                // distance along both axes must fit in an i32).
296                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                // NOTE: We *could* just scan forward until we hit the end, but this way we save
301                // a comparison in the inner loop, since also needs to check the
302                // list length.  We could also save some time by starting from
303                // start rather than end, but the hope is that this way the
304                // compiler (and machine) can reorder things so both ends are
305                // fetched in parallel; since the vast majority of the time both fetched
306                // elements should already be in cache, this should not use any
307                // extra memory bandwidth.
308                //
309                // TODO: Benchmark and figure out whether this is better in practice than just
310                // scanning forward.
311                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        // Remove chunks that are too far from players.
331        //
332        // Note that all chunks involved here (both terrain chunks and pending chunks)
333        // are guaranteed in bounds.  This simplifies the rest of the logic
334        // here.
335        let chunks_to_remove = data.terrain
336            .par_keys()
337            .copied()
338            // There may be lots of pending chunks, so don't check them all.  This should be okay
339            // as long as we're maintaining a reasonable tick rate.
340            .chain(data.chunk_generator.par_pending_chunks())
341            // Don't check every chunk every tick (spread over 16 ticks)
342            //
343            // TODO: Investigate whether we can add support for performing this filtering directly
344            // within hashbrown (basically, specify we want to iterate through just buckets with
345            // hashes in a particular range).  This could provide significiant speedups since we
346            // could avoid having to iterate through a bunch of buckets we don't care about.
347            //
348            // TODO: Make the percentage of the buckets that we go through adjust dynamically
349            // depending on the current number of chunks.  In the worst case, we might want to scan
350            // just 1/256 of the chunks each tick, for example.
351            .filter(|k| k.x % 4 + (k.y % 4) * 4 == tick)
352            .filter(|&chunk_key| {
353                // We only have to check players inside the maximum view distance of the server of
354                // our own position.
355                //
356                // We start by partitioning by X, finding only entities in chunks within the X
357                // range of us.  These are guaranteed in bounds due to restrictions on max view
358                // distance (namely: the square of any chunk coordinate plus the max view distance
359                // along both axes must fit in an i32).
360                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                // NOTE: We *could* just scan forward until we hit the end, but this way we save a
365                // comparison in the inner loop, since also needs to check the list length.  We
366                // could also save some time by starting from start rather than end, but the hope
367                // is that this way the compiler (and machine) can reorder things so both ends are
368                // fetched in parallel; since the vast majority of the time both fetched elements
369                // should already be in cache, this should not use any extra memory bandwidth.
370                //
371                // TODO: Benchmark and figure out whether this is better in practice than just
372                // scanning forward.
373                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                // Register the unloading of this chunk from terrain persistence
386                #[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                // If you want to trigger any behaivour on unload, do it in `Server::tick` by
394                // reading `TerrainChanges::removed_chunks` since chunks can also be removed
395                // using eg. /reload_chunks
396
397                // TODO: code duplication for chunk insertion between here and state.rs
398                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            // Drop chunks in a background thread.
405            data.slow_jobs.spawn("CHUNK_DROP", move || {
406                drop(chunks_to_remove);
407            });
408        }
409    }
410}
411
412// TODO: better name
413#[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/// Convinient structure to use when you need to create new npc
433/// from EntityInfo
434// TODO: better name?
435// TODO: if this is send around network, optimize the large_enum_variant
436#[expect(clippy::large_enum_variant)] // TODO: evaluate
437#[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            // flags
447            special_entity,
448            has_agency,
449            agent_mark,
450            alignment,
451            no_flee,
452            idle_wander_factor,
453            aggro_range_multiplier,
454            // stats
455            body,
456            name,
457            scale,
458            pos,
459            loot,
460            // tools and skills
461            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            // Evaluate lazy function for loadout creation
490            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        // Allow Humanoid, BirdMedium, and Parrot to speak
518        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            // Non-humanoids get a patrol origin to stop them moving too far
545            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    // Hardcoded max VD to prevent stupid view distances from creating overflows.
660    // This must be a value ≤
661    // √(i32::MAX - 2 * ((1 << (MAX_WORLD_BLOCKS_LG - TERRAIN_CHUNK_BLOCKS_LG) - 1)²
662    // - 1)) / 2
663    //
664    // since otherwise we could end up overflowing.  Since it is a requirement that
665    // each dimension (in chunks) has to fit in a i16, we can derive √((1<<31)-1
666    // - 2*((1<<15)-1)^2) / 2 ≥ 1 << 7 as the absolute limit.
667    //
668    // TODO: Make this more official and use it elsewhere.
669    const MAX_VD: u32 = 1 << 7;
670
671    // This fuzzy threshold prevents chunks rapidly unloading and reloading when
672    // players move over a chunk border.
673    const UNLOAD_THRESHOLD: u32 = 2;
674
675    // NOTE: This cast is safe for the reasons mentioned above.
676    (vd.clamp(crate::MIN_VD, max_view_distance)
677        .saturating_add(UNLOAD_THRESHOLD))
678    .min(MAX_VD) as i32
679}
680
681/// Returns: ((player_chunk_pos, player_vd_squared), entity, is_client)
682fn 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    // NOTE: We use regular as casts rather than as_ because we want to saturate on
695    // overflow.
696    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    // We filter out positions that are *clearly* way out of range from
701    // consideration. This is pretty easy to do, and means we don't have to
702    // perform expensive overflow checks elsewhere (otherwise, a player
703    // sufficiently far off the map could cause chunks they were nowhere near to
704    // stay loaded, parallel universes style).
705    //
706    // One could also imagine snapping a player to the part of the map nearest to
707    // them. We don't currently do this in case we rely elsewhere on players
708    // always being near the chunks they're keeping loaded, but it would allow
709    // us to use u32 exclusively so it's tempting.
710    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        // The cast to i32 here is definitely safe thanks to MAX_VD limiting us to fit
720        // within i32^2.
721        //
722        // The cast from each coordinate to i16 should also be correct here.  This is because valid
723        // world chunk coordinates are no greater than 1 << 14 - 1; since we verified that the
724        // player is within world bounds modulo player_vd, which is guaranteed to never let us
725        // overflow an i16 when added to a u14, safety of the cast follows.
726        .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    // We start by collecting presences and positions from players, because they are
741    // very sparse in the entity list and therefore iterating over them for each
742    // chunk can be quite slow.
743    let world_aabr_in_chunks = Aabr {
744        min: Vec2::zero(),
745        // NOTE: Cast is correct because chunk coordinates must fit in an i32 (actually, i16).
746        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                // For chunks with clients, we need to record their entity, because they might
764                // be used for insertion.  These elements fit in 8 bytes, so
765                // this should be pretty cache-friendly.
766                if is_client {
767                    Either::Left((player_data, entity))
768                } else {
769                    // For chunks without clients, we only need to record the position and view
770                    // distance.  These elements fit in 4 bytes, which is even cache-friendlier.
771                    Either::Right(player_data)
772                }
773            });
774
775    // We sort the presence lists by X position, so we can efficiently filter out
776    // players nowhere near the chunk.  This is basically a poor substitute for
777    // the effects of a proper KDTree, but a proper KDTree has too much overhead
778    // to be worth using for such a short list (~ 1000 players at most).  We
779    // also sort by y and reverse view distance; this will become important later.
780    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    // For the vast majority of chunks (present and pending ones), we'll only ever
784    // need the position and view distance.  So we extend it with these from the
785    // list of client chunks, and then do some further work to improve
786    // performance (taking advantage of the fact that they don't require
787    // entities).
788    presences_positions.extend(
789        presences_positions_entities
790            .iter()
791            .map(|&(player_data, _)| player_data),
792    );
793    // Since both lists were previously sorted, we use stable sort over unstable
794    // sort, as it's faster in that case (theoretically a proper merge operation
795    // would be ideal, but it's not worth pulling in a library for).
796    presences_positions.sort_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
797    // Now that the list is sorted, we deduplicate players in the same chunk (this
798    // is why we need to sort y as well as x; dedup only works if the list is
799    // sorted by the element we use to dedup).  Importantly, we can then use
800    // only the *first* element as a substitute for all the players in the
801    // chunk, because we *also* sorted from greatest to lowest view
802    // distance, and dedup_by removes all but the first matching element.  In the
803    // common case where a few chunks are very crowded, this further reduces the
804    // work required per chunk.
805    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    // NOTE: Guaranteed in bounds as long as prepare_player_presences prepared the
812    // player_chunk_pos and player_vd_sqr.
813    let adjusted_dist_sqr = (player_chunk_pos.as_::<i32>() - chunk_pos).magnitude_squared();
814
815    adjusted_dist_sqr <= player_vd_sqr
816}