address review comments

This commit is contained in:
crabman 2024-03-01 07:24:45 +00:00
parent 51152f47fa
commit d58f7f0931
No known key found for this signature in database
12 changed files with 245 additions and 161 deletions

View File

@ -14,7 +14,7 @@
]), None)),
)),
),
pets: [("common.entity.dungeon.cultist.hound", 4, 5)],
pets: [("common.entity.dungeon.cultist.hound", ( start: 4, end: 5 ))],
meta: [
SkillSetAsset("common.skillset.preset.rank5.fullskill"),
],

View File

@ -14,7 +14,7 @@ use std::{collections::VecDeque, fmt};
use strum::{EnumIter, IntoEnumIterator};
use vek::*;
use super::{dialogue::Subject, Pos};
use super::{dialogue::Subject, Group, Pos};
pub const DEFAULT_INTERACTION_TIME: f32 = 3.0;
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
@ -112,6 +112,18 @@ impl Alignment {
_ => false,
}
}
/// Default group for this alignment
// NOTE: This is a HACK
pub fn group(&self) -> Option<Group> {
match self {
Alignment::Wild => None,
Alignment::Passive => None,
Alignment::Enemy => Some(super::group::ENEMY),
Alignment::Npc | Alignment::Tame => Some(super::group::NPC),
Alignment::Owned(_) => None,
}
}
}
impl Component for Alignment {

View File

@ -3,6 +3,7 @@ use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use slab::Slab;
use specs::{storage::GenericReadStorage, Component, DerefFlaggedStorage, Join, LendJoin};
use std::iter;
use tracing::{error, warn};
// Primitive group system
@ -309,19 +310,6 @@ impl GroupManager {
return;
}
self.remove_from_group(member, groups, alignments, uids, entities, notifier, false);
// Set NPC back to their group
if let Some(alignment) = alignments.get(member) {
match alignment {
Alignment::Npc => {
let _ = groups.insert(member, NPC);
},
Alignment::Enemy => {
let _ = groups.insert(member, ENEMY);
},
_ => {},
}
}
}
pub fn entity_deleted(
@ -367,7 +355,7 @@ impl GroupManager {
.join()
.filter(|(e, _, g, _)| **g == group && !(to_be_deleted && *e == member))
.fold(
HashMap::<Uid, (Option<specs::Entity>, Vec<specs::Entity>)>::new(),
HashMap::<Uid, (Option<specs::Entity>, Option<Alignment>, Vec<specs::Entity>)>::new(),
|mut acc, (e, uid, _, alignment)| {
if let Some(owner) = alignment.and_then(|a| match a {
Alignment::Owned(owner) if uid != owner => Some(owner),
@ -375,10 +363,14 @@ impl GroupManager {
}) {
// A pet
// Assumes owner will be in the group
acc.entry(*owner).or_default().1.push(e);
acc.entry(*owner).or_default().2.push(e);
} else {
// Not a pet
acc.entry(*uid).or_default().0 = Some(e);
//
// Entry may already exist from inserting pets
let entry = acc.entry(*uid).or_default();
entry.0 = Some(e);
entry.1 = alignment.copied();
}
acc
@ -386,9 +378,15 @@ impl GroupManager {
)
.into_iter()
.map(|(_, v)| v)
.for_each(|(owner, pets)| {
.for_each(|(owner, alignment, pets)| {
if let Some(owner) = owner {
if !pets.is_empty() {
if let Some(special_group) = alignment.and_then(|a| a.group()) {
// Set NPC and pets back to their special alignment based group
for entity in iter::once(owner).chain(pets) {
groups.insert(entity, special_group)
.expect("entity from join above so it must exist");
}
} else if !pets.is_empty() {
let mut members =
pets.iter().map(|e| (*e, Role::Pet)).collect::<Vec<_>>();
members.push((owner, Role::Member));
@ -415,8 +413,8 @@ impl GroupManager {
});
}
});
// Not leader
} else {
// Not leader
let leaving_member_uid = if let Some(uid) = uids.get(member) {
*uid
} else {
@ -426,8 +424,24 @@ impl GroupManager {
let leaving_pets = pets(member, leaving_member_uid, alignments, entities);
// If pets and not about to be deleted form new group
if !leaving_pets.is_empty() && !to_be_deleted {
// Handle updates for the leaving group member and their pets.
if let Some(special_group) = alignments.get(member).and_then(|a| a.group())
&& !to_be_deleted
{
// Set NPC and pets back to their the special alignment based group
for entity in iter::once(member).chain(leaving_pets.iter().copied()) {
groups
.insert(entity, special_group)
.expect("entity from join above so it must exist");
}
// Form new group if these conditions are met:
// * Alignment not for a special npc group (handled above)
// * The entity has pets.
// * The entity isn't about to be deleted.
} else if !leaving_pets.is_empty()
&& !to_be_deleted
&& alignments.get(member).and_then(Alignment::group).is_none()
{
let new_group = self.create_group(member, 1);
notifier(member, ChangeNotification::NewGroup {
@ -435,7 +449,7 @@ impl GroupManager {
members: leaving_pets
.iter()
.map(|p| (*p, Role::Pet))
.chain(std::iter::once((member, Role::Member)))
.chain(iter::once((member, Role::Member)))
.collect(),
});
@ -451,6 +465,8 @@ impl GroupManager {
});
}
// Handle updates for the rest of the group, potentially disbanding it if there
// is now only one member left.
if let Some(info) = self.group_info_mut(group) {
// If not pet, decrement number of members
if !matches!(alignments.get(member), Some(Alignment::Owned(owner)) if uids.get(member).map_or(true, |uid| uid != owner))

View File

@ -1,3 +1,5 @@
use std::ops::RangeInclusive;
use crate::{
assets::{self, AssetExt, Error},
calendar::Calendar,
@ -15,6 +17,7 @@ use crate::{
};
use enum_map::EnumMap;
use serde::Deserialize;
use tracing::error;
use vek::*;
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
@ -136,9 +139,10 @@ pub struct EntityConfig {
/// Check docs for `InventorySpec` struct in this file.
pub inventory: InventorySpec,
/// Pets to spawn with this entity (specified as a list of asset paths)
/// Pets to spawn with this entity (specified as a list of asset paths).
/// The range represents how many pets will be spawned.
#[serde(default)]
pub pets: Vec<(String, usize, usize)>,
pub pets: Vec<(String, RangeInclusive<usize>)>,
/// Meta Info for optional fields
/// Possible fields:
@ -320,18 +324,25 @@ impl EntityInfo {
// NOTE: set loadout after body, as it's used with default equipement
self = self.with_inventory(inventory, config_asset, loadout_rng, time);
for (pet_asset, start, end) in pets {
let config = EntityConfig::load_expect_cloned(&pet_asset);
self.pets
.extend((0..loadout_rng.gen_range(start..=end)).map(|_| {
EntityInfo::at(self.pos).with_entity_config(
config.clone(),
config_asset,
loadout_rng,
time,
)
}));
let mut pet_infos: Vec<EntityInfo> = Vec::new();
for (pet_asset, amount) in pets {
let config = EntityConfig::load_expect(&pet_asset).read();
let (start, mut end) = amount.into_inner();
if start > end {
error!("Invalid range for pet count start: {start}, end: {end}");
end = start;
}
pet_infos.extend((0..loadout_rng.gen_range(start..=end)).map(|_| {
EntityInfo::at(self.pos).with_entity_config(
config.clone(),
config_asset,
loadout_rng,
time,
)
}));
}
self.pets = pet_infos;
// Prefer the new configuration, if possible
let AgentConfig {
@ -693,12 +704,22 @@ mod tests {
}
#[cfg(test)]
fn validate_pets(pets: Vec<(String, usize, usize)>, config_asset: &str) {
for pet in pets.into_iter().map(|(pet_asset, _, _)| {
EntityConfig::load_cloned(&pet_asset).unwrap_or_else(|_| {
panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
})
fn validate_pets(pets: Vec<(String, RangeInclusive<usize>)>, config_asset: &str) {
for (pet, amount) in pets.into_iter().map(|(pet_asset, amount)| {
(
EntityConfig::load_cloned(&pet_asset).unwrap_or_else(|_| {
panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
}),
amount,
)
}) {
assert!(
amount.end() >= amount.start(),
"Invalid pet spawn range ({}..={}), in {}",
amount.start(),
amount.end(),
config_asset
);
if !pet.pets.is_empty() {
panic!("Pets must not be owners of pets: {config_asset}");
}

View File

@ -9,7 +9,7 @@ use crate::{
server_description::ServerDescription, Ban, BanAction, BanInfo, EditableSetting,
SettingError, WhitelistInfo, WhitelistRecord,
},
sys::terrain::NpcData,
sys::terrain::SpawnEntityData,
weather::WeatherJob,
wiring,
wiring::OutputFormula,
@ -726,17 +726,15 @@ fn handle_make_npc(
None,
);
match NpcData::from_entity_info(entity_info) {
NpcData::Waypoint(_) => {
match SpawnEntityData::from_entity_info(entity_info) {
SpawnEntityData::Waypoint(_) => {
return Err(Content::localized("command-unimplemented-waypoint-spawn"));
},
NpcData::Teleporter(_, _) => {
SpawnEntityData::Teleporter(_, _) => {
return Err(Content::localized("command-unimplemented-teleporter-spawn"));
},
data @ NpcData::Data { .. } => {
let (npc_builder, _pos) = data
.to_npc_builder()
.expect("We know this NpcData is valid");
SpawnEntityData::Npc(data) => {
let (npc_builder, _pos) = data.to_npc_builder();
server
.state
@ -1735,13 +1733,7 @@ fn handle_spawn(
owner_entity: target,
pet_entity: new_entity,
});
} else if let Some(group) = match alignment {
Alignment::Wild => None,
Alignment::Passive => None,
Alignment::Enemy => Some(comp::group::ENEMY),
Alignment::Npc | Alignment::Tame => Some(comp::group::NPC),
comp::Alignment::Owned(_) => unreachable!(),
} {
} else if let Some(group) = alignment.group() {
insert_or_replace_component(server, new_entity, group, "new entity")?;
}

View File

@ -198,13 +198,7 @@ pub fn handle_create_npc(server: &mut Server, mut ev: CreateNpcEvent) -> EcsEnti
},
);
}
} else if let Some(group) = match ev.npc.alignment {
Alignment::Wild => None,
Alignment::Passive => None,
Alignment::Enemy => Some(comp::group::ENEMY),
Alignment::Npc | Alignment::Tame => Some(comp::group::NPC),
comp::Alignment::Owned(_) => unreachable!(),
} {
} else if let Some(group) = ev.npc.alignment.group() {
let _ = server.state.ecs().write_storage().insert(new_entity, group);
}

View File

@ -11,7 +11,7 @@ use crate::{
pet::tame_pet,
rtsim::RtSim,
state_ext::StateExt,
sys::terrain::{NpcData, SAFE_ZONE_RADIUS},
sys::terrain::{NpcData, SpawnEntityData, SAFE_ZONE_RADIUS},
Server, Settings, SpawnPoint,
};
use common::{
@ -2143,8 +2143,8 @@ pub fn transform_entity(
.read_storage::<comp::Player>()
.contains(entity);
match NpcData::from_entity_info(entity_info) {
NpcData::Data {
match SpawnEntityData::from_entity_info(entity_info) {
SpawnEntityData::Npc(NpcData {
inventory,
stats,
skill_set,
@ -2157,7 +2157,7 @@ pub fn transform_entity(
alignment: _,
pos: _,
pets,
} => {
}) => {
fn set_or_remove_component<C: specs::Component>(
server: &mut Server,
entity: EcsEntity,
@ -2254,9 +2254,10 @@ pub fn transform_entity(
// Spawn pets
let position = server.state.read_component_copied::<comp::Pos>(entity);
if let Some(pos) = position {
for (pet, offset) in pets.into_iter().filter_map(|(pet, offset)| {
pet.to_npc_builder().map(|(pet, _)| (pet, offset)).ok()
}) {
for (pet, offset) in pets
.into_iter()
.map(|(pet, offset)| (pet.to_npc_builder().0, offset))
{
let pet_entity = handle_create_npc(server, CreateNpcEvent {
pos: comp::Pos(pos.0 + offset),
ori: comp::Ori::from_unnormalized_vec(offset).unwrap_or_default(),
@ -2268,10 +2269,10 @@ pub fn transform_entity(
}
}
},
NpcData::Waypoint(_) => {
SpawnEntityData::Waypoint(_) => {
return Err(TransformEntityError::UnexpectedNpcWaypoint);
},
NpcData::Teleporter(_, _) => {
SpawnEntityData::Teleporter(_, _) => {
return Err(TransformEntityError::UnexpectedNpcTeleporter);
},
}

View File

@ -105,6 +105,7 @@ pub struct InventoryManipData<'a> {
orientations: ReadStorage<'a, comp::Ori>,
controllers: ReadStorage<'a, comp::Controller>,
agents: ReadStorage<'a, comp::Agent>,
pets: ReadStorage<'a, comp::Pet>,
velocities: ReadStorage<'a, comp::Vel>,
}
@ -520,9 +521,9 @@ impl ServerEvent for InventoryManipEvent {
const MAX_PETS: usize = 3;
let reinsert = if let Some(pos) = data.positions.get(entity)
{
if (&data.alignments, &data.agents)
if (&data.alignments, &data.agents, data.pets.mask())
.join()
.filter(|(alignment, _)| {
.filter(|(alignment, _, _)| {
alignment == &&comp::Alignment::Owned(*uid)
})
.count()

View File

@ -838,7 +838,16 @@ impl Server {
}
}
// Prevent anchor entity chains which are not currently supported
// Prevent anchor entity chains which are not currently supported due to:
// * potential cycles?
// * unloading a chain could occur across an unbounded number of ticks with the
// current implementation.
// * in particular, we want to be able to unload all entities in a
// limited number of ticks when a database error occurs and kicks all
// players (not quiet sure on exact time frame, since it already
// takes a tick after unloading all chunks for entities to despawn?),
// see this thread and the discussion linked from there:
// https://gitlab.com/veloren/veloren/-/merge_requests/2668#note_634913847
let anchors = self.state.ecs().read_storage::<Anchor>();
let anchored_anchor_entities: Vec<Entity> = (
&self.state.ecs().entities(),
@ -849,8 +858,13 @@ impl Server {
Anchor::Entity(anchor_entity) => Some(*anchor_entity),
_ => None,
})
// We allow Anchor::Entity(_) -> Anchor::Chunk(_) chains
.filter(|anchor_entity| matches!(anchors.get(*anchor_entity), Some(Anchor::Entity(_))))
// We allow Anchor::Entity(_) -> Anchor::Chunk(_) connections, since they can't chain further.
//
// NOTE: The entity with `Anchor::Entity` will unload one tick after the entity with `Anchor::Chunk`.
.filter(|anchor_entity| match anchors.get(*anchor_entity) {
Some(Anchor::Entity(_)) => true,
Some(Anchor::Chunk(_)) | None => false
})
.collect();
drop(anchors);

View File

@ -7,8 +7,8 @@ use common::{
uid::Uid,
};
use common_net::msg::ServerGeneral;
use specs::{Entity, WorldExt};
use tracing::warn;
use specs::{Entity, Join, WorldExt};
use tracing::{error, warn};
/// Restores a pet retrieved from the database on login, assigning it to its
/// owner
@ -23,31 +23,43 @@ pub fn tame_pet(ecs: &specs::World, pet_entity: Entity, owner: Entity) {
fn tame_pet_internal(ecs: &specs::World, pet_entity: Entity, owner: Entity, pet: Option<Pet>) {
let uids = ecs.read_storage::<Uid>();
let owner_uid = match uids.get(owner) {
Some(uid) => *uid,
None => return,
let (owner_uid, pet_uid) = match (uids.get(owner), uids.get(pet_entity)) {
(Some(uid_owner), Some(uid_pet)) => (*uid_owner, *uid_pet),
_ => return,
};
let mut alignments = ecs.write_storage::<Alignment>();
let Some(owner_alignment) = alignments.get(owner).copied() else {
error!("Owner of a pet must have an Alignment");
return;
};
if let Some(Alignment::Owned(existing_owner_uid)) =
ecs.read_storage::<Alignment>().get(pet_entity)
{
if let Some(Alignment::Owned(existing_owner_uid)) = alignments.get(pet_entity) {
if *existing_owner_uid != owner_uid {
warn!("Disallowing taming of pet already owned by another entity");
return;
}
}
if let Some(Alignment::Owned(owner_alignment_uid)) = ecs.read_storage::<Alignment>().get(owner)
{
if *owner_alignment_uid != owner_uid {
warn!("Pets cannot be owners of pets");
if let Alignment::Owned(owner_alignment_uid) = owner_alignment {
if owner_alignment_uid != owner_uid {
error!("Pets cannot be owners of pets");
return;
}
}
let _ = ecs
.write_storage()
.insert(pet_entity, common::comp::Alignment::Owned(owner_uid));
if (
&ecs.entities(),
&alignments,
ecs.read_storage::<Pet>().mask(),
)
.join()
.any(|(_, alignment, _)| matches!(alignment, Alignment::Owned(uid) if *uid == pet_uid))
{
error!("Cannot tame entity which owns pets");
return;
}
let _ = alignments.insert(pet_entity, common::comp::Alignment::Owned(owner_uid));
// Anchor the pet to the player to prevent it de-spawning
// when its chunk is unloaded if its owner is still logged
@ -78,6 +90,8 @@ fn tame_pet_internal(ecs: &specs::World, pet_entity: Entity, owner: Entity, pet:
let clients = ecs.read_storage::<Client>();
let mut group_manager = ecs.write_resource::<GroupManager>();
let map_markers = ecs.read_storage::<comp::MapMarker>();
drop(alignments);
group_manager.new_pet(
pet_entity,
owner,

View File

@ -1,7 +1,7 @@
#![allow(dead_code)] // TODO: Remove this when rtsim is fleshed out
use super::*;
use crate::sys::terrain::NpcData;
use crate::sys::terrain::SpawnEntityData;
use common::{
calendar::Calendar,
comp::{self, Body, Presence, PresenceKind},
@ -339,9 +339,10 @@ impl<'a> System<'a> for Sys {
Some(&calendar_data),
);
let (mut npc_builder, pos) = NpcData::from_entity_info(entity_info)
.to_npc_builder()
.expect("NpcData must be valid");
let (mut npc_builder, pos) = SpawnEntityData::from_entity_info(entity_info)
.into_npc_data_inner()
.expect("Entity loaded from assets cannot be special")
.to_npc_builder();
if let Some(agent) = &mut npc_builder.agent {
agent.rtsim_outbox = Some(Default::default());
@ -378,9 +379,13 @@ impl<'a> System<'a> for Sys {
Some(&calendar_data),
);
let mut npc_builder = NpcData::from_entity_info(entity_info)
let mut npc_builder = SpawnEntityData::from_entity_info(entity_info)
.into_npc_data_inner()
// EntityConfig can't represent Waypoints at all
// as of now, and if someone will try to spawn
// rtsim waypoint it is definitely error.
.expect("Entity loaded from assets cannot be special")
.to_npc_builder()
.expect("NpcData must be valid")
.0
.with_rtsim(RtSimEntity(npc_id));

View File

@ -2,6 +2,7 @@
use crate::test_world::{IndexOwned, World};
#[cfg(feature = "persistent_world")]
use crate::TerrainPersistence;
use tracing::error;
#[cfg(feature = "worldgen")]
use world::{IndexOwned, World};
@ -204,15 +205,13 @@ impl<'a> System<'a> for Sys {
"Chunk spawned entity that wasn't nearby",
);
let data = NpcData::from_entity_info(entity);
let data = SpawnEntityData::from_entity_info(entity);
match data {
NpcData::Waypoint(pos) => {
SpawnEntityData::Waypoint(pos) => {
emitters.emit(CreateWaypointEvent(pos));
},
data @ NpcData::Data { .. } => {
let (npc_builder, pos) = data
.to_npc_builder()
.expect("This NpcData is known to be valid");
SpawnEntityData::Npc(data) => {
let (npc_builder, pos) = data.to_npc_builder();
emitters.emit(CreateNpcEvent {
pos,
@ -221,7 +220,7 @@ impl<'a> System<'a> for Sys {
rider: None,
});
},
NpcData::Teleporter(pos, teleporter) => {
SpawnEntityData::Teleporter(pos, teleporter) => {
emitters.emit(CreateTeleporterEvent(pos, teleporter));
},
}
@ -399,32 +398,36 @@ impl<'a> System<'a> for Sys {
}
}
// TODO: better name
#[derive(Debug)]
pub struct NpcData {
pub pos: Pos,
pub stats: comp::Stats,
pub skill_set: comp::SkillSet,
pub health: Option<comp::Health>,
pub poise: comp::Poise,
pub inventory: comp::inventory::Inventory,
pub agent: Option<comp::Agent>,
pub body: comp::Body,
pub alignment: comp::Alignment,
pub scale: comp::Scale,
pub loot: LootSpec<String>,
pub pets: Vec<(NpcData, Vec3<f32>)>,
}
/// Convinient structure to use when you need to create new npc
/// from EntityInfo
// TODO: better name?
// TODO: if this is send around network, optimize the large_enum_variant
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum NpcData {
Data {
pos: Pos,
stats: comp::Stats,
skill_set: comp::SkillSet,
health: Option<comp::Health>,
poise: comp::Poise,
inventory: comp::inventory::Inventory,
agent: Option<comp::Agent>,
body: comp::Body,
alignment: comp::Alignment,
scale: comp::Scale,
loot: LootSpec<String>,
pets: Vec<(NpcData, Vec3<f32>)>,
},
pub enum SpawnEntityData {
Npc(NpcData),
Waypoint(Vec3<f32>),
Teleporter(Vec3<f32>, PortalData),
}
impl NpcData {
impl SpawnEntityData {
pub fn from_entity_info(entity: EntityInfo) -> Self {
let EntityInfo {
// flags
@ -447,7 +450,6 @@ impl NpcData {
inventory: items,
make_loadout,
trading_information: economy,
// unused
pets,
} = entity;
@ -548,7 +550,7 @@ impl NpcData {
agent
};
NpcData::Data {
SpawnEntityData::Npc(NpcData {
pos: Pos(pos),
stats,
skill_set,
@ -564,57 +566,69 @@ impl NpcData {
let pet_count = pets.len() as f32;
pets.into_iter()
.enumerate()
.map(|(i, pet)| {
(
NpcData::from_entity_info(pet),
.flat_map(|(i, pet)| {
Some((
SpawnEntityData::from_entity_info(pet)
.into_npc_data_inner()
.inspect_err(|data| {
error!("Pets must be SpawnEntityData::Npc, but found: {data:?}")
})
.ok()?,
Vec2::one()
.rotated_z(TAU * (i as f32 / pet_count))
.with_z(0.0)
* ((pet_count * 3.0) / TAU),
)
))
})
.collect()
},
}
})
}
#[allow(clippy::result_large_err)]
pub fn to_npc_builder(self) -> Result<(NpcBuilder, comp::Pos), Self> {
pub fn into_npc_data_inner(self) -> Result<NpcData, Self> {
match self {
NpcData::Data {
pos,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
loot,
pets,
} => Ok((
NpcBuilder::new(stats, body, alignment)
.with_skill_set(skill_set)
.with_health(health)
.with_poise(poise)
.with_inventory(inventory)
.with_agent(agent)
.with_scale(scale)
.with_loot(loot)
.with_pets(
pets.into_iter()
.map(|(pet, offset)| pet.to_npc_builder().map(|(pet, _)| (pet, offset)))
.collect::<Result<Vec<_>, _>>()?,
),
pos,
)),
NpcData::Waypoint(_) | NpcData::Teleporter(_, _) => Err(self),
SpawnEntityData::Npc(inner) => Ok(inner),
other => Err(other),
}
}
}
impl NpcData {
pub fn to_npc_builder(self) -> (NpcBuilder, comp::Pos) {
let NpcData {
pos,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
loot,
pets,
} = self;
(
NpcBuilder::new(stats, body, alignment)
.with_skill_set(skill_set)
.with_health(health)
.with_poise(poise)
.with_inventory(inventory)
.with_agent(agent)
.with_scale(scale)
.with_loot(loot)
.with_pets(
pets.into_iter()
.map(|(pet, offset)| (pet.to_npc_builder().0, offset))
.collect::<Vec<_>>(),
),
pos,
)
}
}
pub fn convert_to_loaded_vd(vd: u32, max_view_distance: u32) -> i32 {
// Hardcoded max VD to prevent stupid view distances from creating overflows.
// This must be a value ≤