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