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,
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/// This system will handle loading generated chunks and unloading
94/// unneeded chunks.
95///     1. Inserts newly generated chunks into the TerrainGrid
96///     2. Sends new chunks to nearby clients
97///     3. Handles the chunk's supplement (e.g. npcs)
98///     4. Removes chunks outside the range of players
99#[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        // Generate requested chunks
112        //
113        // Submit requests for chunks right before receiving finished chunks so that we
114        // don't create duplicate work for chunks that just finished but are not
115        // yet added to the terrain.
116        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        // Fetch any generated `TerrainChunk`s and insert them into the terrain.
130        // Also, send the chunk data to anybody that is close by.
131        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            // Apply changes from terrain persistence to this chunk
151            #[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            // Arcify the chunk
157            let chunk = Arc::new(chunk);
158
159            // Add to list of chunks to send to nearby players.
160            new_chunks.push(key);
161
162            // TODO: code duplication for chunk insertion between here and state.rs
163            // Insert the chunk into terrain changes
164            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            // Handle chunk supplement
174            for entity in supplement.entities {
175                // Check this because it's a common source of weird bugs
176                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                            rider: None,
197                        });
198                    },
199                }
200            }
201        }
202
203        // TODO: Consider putting this in another system since this forces us to take
204        // positions by write rather than read access.
205        let repositioned = (&data.entities, &mut data.positions, (&mut data.forced_updates).maybe(), &data.reposition_on_load)
206            // TODO: Consider using par_bridge() because Rayon has very poor work splitting for
207            // sparse joins.
208            .par_join()
209            .filter_map(|(entity, pos, force_update, reposition)| {
210                // NOTE: We use regular as casts rather than as_ because we want to saturate on
211                // overflow.
212                let entity_pos = pos.0.map(|x| x as i32);
213                // If an entity is marked as needing repositioning once the chunk loads (e.g.
214                // from having just logged in), reposition them.
215                let chunk_pos = TerrainGrid::chunk_key(entity_pos);
216                let chunk = data.terrain.get_key(chunk_pos)?;
217                let new_pos = if reposition.needs_ground {
218                    data.terrain.try_find_ground(entity_pos)
219                } else {
220                    data.terrain.try_find_space(entity_pos)
221                }.map(|x| x.as_::<f32>()).unwrap_or_else(|| chunk.find_accessible_pos(entity_pos.xy(), false));
222                pos.0 = new_pos;
223                force_update.map(|force_update| force_update.update());
224                Some((entity, new_pos))
225            })
226            .collect::<Vec<_>>();
227
228        for (entity, new_pos) in repositioned {
229            if let Some(waypoint) = data.waypoints.get_mut(entity) {
230                *waypoint = Waypoint::new(new_pos, *data.time);
231            }
232            data.reposition_on_load.remove(entity);
233        }
234
235        let max_view_distance = data.server_settings.max_view_distance.unwrap_or(u32::MAX);
236        #[cfg(feature = "worldgen")]
237        let world_size = data.world.sim().get_size();
238        #[cfg(not(feature = "worldgen"))]
239        let world_size = data.world.map_size_lg().chunks().map(u32::from);
240        let (presences_position_entities, presences_positions) = prepare_player_presences(
241            world_size,
242            max_view_distance,
243            &data.entities,
244            &data.positions,
245            &data.presences,
246            &data.clients,
247        );
248        let real_max_view_distance = convert_to_loaded_vd(u32::MAX, max_view_distance);
249
250        // Send the chunks to all nearby players.
251        new_chunks.par_iter().for_each_init(
252            || data.chunk_send_bus.emitter(),
253            |chunk_send_emitter, chunk_key| {
254                // We only have to check players inside the maximum view distance of the server
255                // of our own position.
256                //
257                // We start by partitioning by X, finding only entities in chunks within the X
258                // range of us.  These are guaranteed in bounds due to restrictions on max view
259                // distance (namely: the square of any chunk coordinate plus the max view
260                // distance along both axes must fit in an i32).
261                let min_chunk_x = chunk_key.x - real_max_view_distance;
262                let max_chunk_x = chunk_key.x + real_max_view_distance;
263                let start = presences_position_entities
264                    .partition_point(|((pos, _), _)| i32::from(pos.x) < min_chunk_x);
265                // NOTE: We *could* just scan forward until we hit the end, but this way we save
266                // a comparison in the inner loop, since also needs to check the
267                // list length.  We could also save some time by starting from
268                // start rather than end, but the hope is that this way the
269                // compiler (and machine) can reorder things so both ends are
270                // fetched in parallel; since the vast majority of the time both fetched
271                // elements should already be in cache, this should not use any
272                // extra memory bandwidth.
273                //
274                // TODO: Benchmark and figure out whether this is better in practice than just
275                // scanning forward.
276                let end = presences_position_entities
277                    .partition_point(|((pos, _), _)| i32::from(pos.x) < max_chunk_x);
278                let interior = &presences_position_entities[start..end];
279                interior
280                    .iter()
281                    .filter(|((player_chunk_pos, player_vd_sqr), _)| {
282                        chunk_in_vd(*player_chunk_pos, *player_vd_sqr, *chunk_key)
283                    })
284                    .for_each(|(_, entity)| {
285                        chunk_send_emitter.emit(ChunkSendEntry {
286                            entity: *entity,
287                            chunk_key: *chunk_key,
288                        });
289                    });
290            },
291        );
292
293        let tick = (data.tick.0 % 16) as i32;
294
295        // Remove chunks that are too far from players.
296        //
297        // Note that all chunks involved here (both terrain chunks and pending chunks)
298        // are guaranteed in bounds.  This simplifies the rest of the logic
299        // here.
300        let chunks_to_remove = data.terrain
301            .par_keys()
302            .copied()
303            // There may be lots of pending chunks, so don't check them all.  This should be okay
304            // as long as we're maintaining a reasonable tick rate.
305            .chain(data.chunk_generator.par_pending_chunks())
306            // Don't check every chunk every tick (spread over 16 ticks)
307            //
308            // TODO: Investigate whether we can add support for performing this filtering directly
309            // within hashbrown (basically, specify we want to iterate through just buckets with
310            // hashes in a particular range).  This could provide significiant speedups since we
311            // could avoid having to iterate through a bunch of buckets we don't care about.
312            //
313            // TODO: Make the percentage of the buckets that we go through adjust dynamically
314            // depending on the current number of chunks.  In the worst case, we might want to scan
315            // just 1/256 of the chunks each tick, for example.
316            .filter(|k| k.x % 4 + (k.y % 4) * 4 == tick)
317            .filter(|&chunk_key| {
318                // We only have to check players inside the maximum view distance of the server of
319                // our own position.
320                //
321                // We start by partitioning by X, finding only entities in chunks within the X
322                // range of us.  These are guaranteed in bounds due to restrictions on max view
323                // distance (namely: the square of any chunk coordinate plus the max view distance
324                // along both axes must fit in an i32).
325                let min_chunk_x = chunk_key.x - real_max_view_distance;
326                let max_chunk_x = chunk_key.x + real_max_view_distance;
327                let start = presences_positions
328                    .partition_point(|(pos, _)| i32::from(pos.x) < min_chunk_x);
329                // NOTE: We *could* just scan forward until we hit the end, but this way we save a
330                // comparison in the inner loop, since also needs to check the list length.  We
331                // could also save some time by starting from start rather than end, but the hope
332                // is that this way the compiler (and machine) can reorder things so both ends are
333                // fetched in parallel; since the vast majority of the time both fetched elements
334                // should already be in cache, this should not use any extra memory bandwidth.
335                //
336                // TODO: Benchmark and figure out whether this is better in practice than just
337                // scanning forward.
338                let end = presences_positions
339                    .partition_point(|(pos, _)| i32::from(pos.x) < max_chunk_x);
340                let interior = &presences_positions[start..end];
341                !interior.iter().any(|&(player_chunk_pos, player_vd_sqr)| {
342                    chunk_in_vd(player_chunk_pos, player_vd_sqr, chunk_key)
343                })
344            })
345            .collect::<Vec<_>>();
346
347        let chunks_to_remove = chunks_to_remove
348            .into_iter()
349            .filter_map(|key| {
350                // Register the unloading of this chunk from terrain persistence
351                #[cfg(feature = "persistent_world")]
352                if let Some(terrain_persistence) = data.terrain_persistence.as_mut() {
353                    terrain_persistence.unload_chunk(key);
354                }
355
356                data.chunk_generator.cancel_if_pending(key);
357
358                // If you want to trigger any behaivour on unload, do it in `Server::tick` by
359                // reading `TerrainChanges::removed_chunks` since chunks can also be removed
360                // using eg. /reload_chunks
361
362                // TODO: code duplication for chunk insertion between here and state.rs
363                data.terrain.remove(key).inspect(|_| {
364                    data.terrain_changes.removed_chunks.insert(key);
365                })
366            })
367            .collect::<Vec<_>>();
368        if !chunks_to_remove.is_empty() {
369            // Drop chunks in a background thread.
370            data.slow_jobs.spawn("CHUNK_DROP", move || {
371                drop(chunks_to_remove);
372            });
373        }
374    }
375}
376
377// TODO: better name
378#[derive(Debug)]
379pub struct NpcData {
380    pub pos: Pos,
381    pub stats: comp::Stats,
382    pub skill_set: comp::SkillSet,
383    pub health: Option<comp::Health>,
384    pub poise: comp::Poise,
385    pub inventory: comp::inventory::Inventory,
386    pub agent: Option<comp::Agent>,
387    pub body: comp::Body,
388    pub alignment: comp::Alignment,
389    pub scale: comp::Scale,
390    pub loot: LootSpec<String>,
391    pub pets: Vec<(NpcData, Vec3<f32>)>,
392    pub death_effects: Option<DeathEffects>,
393}
394
395/// Convinient structure to use when you need to create new npc
396/// from EntityInfo
397// TODO: better name?
398// TODO: if this is send around network, optimize the large_enum_variant
399#[derive(Debug)]
400pub enum SpawnEntityData {
401    Npc(NpcData),
402    Special(Vec3<f32>, SpecialEntity),
403}
404
405impl SpawnEntityData {
406    pub fn from_entity_info(entity: EntityInfo) -> Self {
407        let EntityInfo {
408            // flags
409            special_entity,
410            has_agency,
411            agent_mark,
412            alignment,
413            no_flee,
414            idle_wander_factor,
415            aggro_range_multiplier,
416            // stats
417            body,
418            name,
419            scale,
420            pos,
421            loot,
422            // tools and skills
423            skillset_asset,
424            loadout: mut loadout_builder,
425            inventory: items,
426            make_loadout,
427            trading_information: economy,
428            pets,
429            death_effects,
430        } = entity;
431
432        if let Some(special) = special_entity {
433            return Self::Special(pos, special);
434        }
435
436        let name = name.unwrap_or_else(|| "Unnamed".to_string());
437        let stats = comp::Stats::new(name, body);
438
439        let skill_set = {
440            let skillset_builder = SkillSetBuilder::default();
441            if let Some(skillset_asset) = skillset_asset {
442                skillset_builder.with_asset_expect(&skillset_asset).build()
443            } else {
444                skillset_builder.build()
445            }
446        };
447
448        let inventory = {
449            // Evaluate lazy function for loadout creation
450            if let Some(make_loadout) = make_loadout {
451                loadout_builder =
452                    loadout_builder.with_creator(make_loadout, economy.as_ref(), None);
453            }
454            let loadout = loadout_builder.build();
455            let mut inventory = comp::inventory::Inventory::with_loadout(loadout, body);
456            for (num, mut item) in items {
457                if let Err(e) = item.set_amount(num) {
458                    tracing::warn!(
459                        "error during creating inventory for {name} at {pos}: {e:?}",
460                        name = &stats.name,
461                    );
462                }
463                if let Err(e) = inventory.push(item) {
464                    tracing::warn!(
465                        "error during creating inventory for {name} at {pos}: {e:?}",
466                        name = &stats.name,
467                    );
468                }
469            }
470
471            inventory
472        };
473
474        let health = Some(comp::Health::new(body));
475        let poise = comp::Poise::new(body);
476
477        // Allow Humanoid, BirdMedium, and Parrot to speak
478        let can_speak = match body {
479            comp::Body::Humanoid(_) => true,
480            comp::Body::BipedSmall(biped_small) => {
481                matches!(biped_small.species, biped_small::Species::Flamekeeper)
482            },
483            comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
484                bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
485                _ => false,
486            },
487            _ => false,
488        };
489
490        let trade_for_site = if matches!(agent_mark, Some(agent::Mark::Merchant)) {
491            economy.map(|e| e.id)
492        } else {
493            None
494        };
495
496        let agent = has_agency.then(|| {
497            let mut agent = comp::Agent::from_body(&body).with_behavior(
498                Behavior::default()
499                    .maybe_with_capabilities(can_speak.then_some(BehaviorCapability::SPEAK))
500                    .maybe_with_capabilities(trade_for_site.map(|_| BehaviorCapability::TRADE))
501                    .with_trade_site(trade_for_site),
502            );
503
504            // Non-humanoids get a patrol origin to stop them moving too far
505            if !matches!(body, comp::Body::Humanoid(_)) {
506                agent = agent.with_patrol_origin(pos);
507            }
508
509            agent
510                .with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee)
511                .with_idle_wander_factor(idle_wander_factor)
512                .with_aggro_range_multiplier(aggro_range_multiplier)
513        });
514
515        let agent = if matches!(alignment, comp::Alignment::Enemy)
516            && matches!(body, comp::Body::Humanoid(_))
517        {
518            agent.map(|a| a.with_aggro_no_warn().with_no_flee_if(true))
519        } else {
520            agent
521        };
522
523        SpawnEntityData::Npc(NpcData {
524            pos: Pos(pos),
525            stats,
526            skill_set,
527            health,
528            poise,
529            inventory,
530            agent,
531            body,
532            alignment,
533            scale: comp::Scale(scale),
534            loot,
535            pets: {
536                let pet_count = pets.len() as f32;
537                pets.into_iter()
538                    .enumerate()
539                    .flat_map(|(i, pet)| {
540                        Some((
541                            SpawnEntityData::from_entity_info(pet)
542                                .into_npc_data_inner()
543                                .inspect_err(|data| {
544                                    error!("Pets must be SpawnEntityData::Npc, but found: {data:?}")
545                                })
546                                .ok()?,
547                            Vec2::one()
548                                .rotated_z(TAU * (i as f32 / pet_count))
549                                .with_z(0.0)
550                                * ((pet_count * 3.0) / TAU),
551                        ))
552                    })
553                    .collect()
554            },
555            death_effects,
556        })
557    }
558
559    pub fn into_npc_data_inner(self) -> Result<NpcData, Self> {
560        match self {
561            SpawnEntityData::Npc(inner) => Ok(inner),
562            other => Err(other),
563        }
564    }
565}
566
567impl NpcData {
568    pub fn to_npc_builder(self) -> (NpcBuilder, comp::Pos) {
569        let NpcData {
570            pos,
571            stats,
572            skill_set,
573            health,
574            poise,
575            inventory,
576            agent,
577            body,
578            alignment,
579            scale,
580            loot,
581            pets,
582            death_effects,
583        } = self;
584
585        (
586            NpcBuilder::new(stats, body, alignment)
587                .with_skill_set(skill_set)
588                .with_health(health)
589                .with_poise(poise)
590                .with_inventory(inventory)
591                .with_agent(agent)
592                .with_scale(scale)
593                .with_loot(loot)
594                .with_pets(
595                    pets.into_iter()
596                        .map(|(pet, offset)| (pet.to_npc_builder().0, offset))
597                        .collect::<Vec<_>>(),
598                )
599                .with_death_effects(death_effects),
600            pos,
601        )
602    }
603}
604
605pub fn convert_to_loaded_vd(vd: u32, max_view_distance: u32) -> i32 {
606    // Hardcoded max VD to prevent stupid view distances from creating overflows.
607    // This must be a value ≤
608    // √(i32::MAX - 2 * ((1 << (MAX_WORLD_BLOCKS_LG - TERRAIN_CHUNK_BLOCKS_LG) - 1)²
609    // - 1)) / 2
610    //
611    // since otherwise we could end up overflowing.  Since it is a requirement that
612    // each dimension (in chunks) has to fit in a i16, we can derive √((1<<31)-1
613    // - 2*((1<<15)-1)^2) / 2 ≥ 1 << 7 as the absolute limit.
614    //
615    // TODO: Make this more official and use it elsewhere.
616    const MAX_VD: u32 = 1 << 7;
617
618    // This fuzzy threshold prevents chunks rapidly unloading and reloading when
619    // players move over a chunk border.
620    const UNLOAD_THRESHOLD: u32 = 2;
621
622    // NOTE: This cast is safe for the reasons mentioned above.
623    (vd.clamp(crate::MIN_VD, max_view_distance)
624        .saturating_add(UNLOAD_THRESHOLD))
625    .min(MAX_VD) as i32
626}
627
628/// Returns: ((player_chunk_pos, player_vd_squared), entity, is_client)
629fn prepare_for_vd_check(
630    world_aabr_in_chunks: &Aabr<i32>,
631    max_view_distance: u32,
632    entity: Entity,
633    presence: &Presence,
634    pos: &Pos,
635    client: Option<u32>,
636) -> Option<((Vec2<i16>, i32), Entity, bool)> {
637    let is_client = client.is_some();
638    let pos = pos.0;
639    let vd = presence.terrain_view_distance.current();
640
641    // NOTE: We use regular as casts rather than as_ because we want to saturate on
642    // overflow.
643    let player_pos = pos.map(|x| x as i32);
644    let player_chunk_pos = TerrainGrid::chunk_key(player_pos);
645    let player_vd = convert_to_loaded_vd(vd, max_view_distance);
646
647    // We filter out positions that are *clearly* way out of range from
648    // consideration. This is pretty easy to do, and means we don't have to
649    // perform expensive overflow checks elsewhere (otherwise, a player
650    // sufficiently far off the map could cause chunks they were nowhere near to
651    // stay loaded, parallel universes style).
652    //
653    // One could also imagine snapping a player to the part of the map nearest to
654    // them. We don't currently do this in case we rely elsewhere on players
655    // always being near the chunks they're keeping loaded, but it would allow
656    // us to use u32 exclusively so it's tempting.
657    let player_aabr_in_chunks = Aabr {
658        min: player_chunk_pos - player_vd,
659        max: player_chunk_pos + player_vd,
660    };
661
662    (world_aabr_in_chunks.max.x >= player_aabr_in_chunks.min.x &&
663     world_aabr_in_chunks.min.x <= player_aabr_in_chunks.max.x &&
664     world_aabr_in_chunks.max.y >= player_aabr_in_chunks.min.y &&
665     world_aabr_in_chunks.min.y <= player_aabr_in_chunks.max.y)
666        // The cast to i32 here is definitely safe thanks to MAX_VD limiting us to fit
667        // within i32^2.
668        //
669        // The cast from each coordinate to i16 should also be correct here.  This is because valid
670        // world chunk coordinates are no greater than 1 << 14 - 1; since we verified that the
671        // player is within world bounds modulo player_vd, which is guaranteed to never let us
672        // overflow an i16 when added to a u14, safety of the cast follows.
673        .then(|| ((player_chunk_pos.as_::<i16>(), player_vd.pow(2)), entity, is_client))
674}
675
676pub fn prepare_player_presences<'a, P>(
677    world_size: Vec2<u32>,
678    max_view_distance: u32,
679    entities: &Entities<'a>,
680    positions: P,
681    presences: &ReadStorage<'a, Presence>,
682    clients: &ReadStorage<'a, Client>,
683) -> (Vec<((Vec2<i16>, i32), Entity)>, Vec<(Vec2<i16>, i32)>)
684where
685    P: GenericReadStorage<Component = Pos> + Join<Type = &'a Pos>,
686{
687    // We start by collecting presences and positions from players, because they are
688    // very sparse in the entity list and therefore iterating over them for each
689    // chunk can be quite slow.
690    let world_aabr_in_chunks = Aabr {
691        min: Vec2::zero(),
692        // NOTE: Cast is correct because chunk coordinates must fit in an i32 (actually, i16).
693        max: world_size.map(|x| x.saturating_sub(1)).as_::<i32>(),
694    };
695
696    let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) =
697        (entities, presences, positions, clients.mask().maybe())
698            .join()
699            .filter_map(|(entity, presence, position, client)| {
700                prepare_for_vd_check(
701                    &world_aabr_in_chunks,
702                    max_view_distance,
703                    entity,
704                    presence,
705                    position,
706                    client,
707                )
708            })
709            .partition_map(|(player_data, entity, is_client)| {
710                // For chunks with clients, we need to record their entity, because they might
711                // be used for insertion.  These elements fit in 8 bytes, so
712                // this should be pretty cache-friendly.
713                if is_client {
714                    Either::Left((player_data, entity))
715                } else {
716                    // For chunks without clients, we only need to record the position and view
717                    // distance.  These elements fit in 4 bytes, which is even cache-friendlier.
718                    Either::Right(player_data)
719                }
720            });
721
722    // We sort the presence lists by X position, so we can efficiently filter out
723    // players nowhere near the chunk.  This is basically a poor substitute for
724    // the effects of a proper KDTree, but a proper KDTree has too much overhead
725    // to be worth using for such a short list (~ 1000 players at most).  We
726    // also sort by y and reverse view distance; this will become important later.
727    presences_positions_entities
728        .sort_unstable_by_key(|&((pos, vd2), _)| (pos.x, pos.y, Reverse(vd2)));
729    presences_positions.sort_unstable_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
730    // For the vast majority of chunks (present and pending ones), we'll only ever
731    // need the position and view distance.  So we extend it with these from the
732    // list of client chunks, and then do some further work to improve
733    // performance (taking advantage of the fact that they don't require
734    // entities).
735    presences_positions.extend(
736        presences_positions_entities
737            .iter()
738            .map(|&(player_data, _)| player_data),
739    );
740    // Since both lists were previously sorted, we use stable sort over unstable
741    // sort, as it's faster in that case (theoretically a proper merge operation
742    // would be ideal, but it's not worth pulling in a library for).
743    presences_positions.sort_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
744    // Now that the list is sorted, we deduplicate players in the same chunk (this
745    // is why we need to sort y as well as x; dedup only works if the list is
746    // sorted by the element we use to dedup).  Importantly, we can then use
747    // only the *first* element as a substitute for all the players in the
748    // chunk, because we *also* sorted from greatest to lowest view
749    // distance, and dedup_by removes all but the first matching element.  In the
750    // common case where a few chunks are very crowded, this further reduces the
751    // work required per chunk.
752    presences_positions.dedup_by_key(|&mut (pos, _)| pos);
753
754    (presences_positions_entities, presences_positions)
755}
756
757pub fn chunk_in_vd(player_chunk_pos: Vec2<i16>, player_vd_sqr: i32, chunk_pos: Vec2<i32>) -> bool {
758    // NOTE: Guaranteed in bounds as long as prepare_player_presences prepared the
759    // player_chunk_pos and player_vd_sqr.
760    let adjusted_dist_sqr = (player_chunk_pos.as_::<i32>() - chunk_pos).magnitude_squared();
761
762    adjusted_dist_sqr <= player_vd_sqr
763}