mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 06:16:04 +00:00
Fix errors on switching dimensions (#204)
* Fix errors on switching dimensions * fix other tests * clippy * fix log feature in test_simulation * fix chunks oops
This commit is contained in:
parent
63b1036a96
commit
833f306e8b
14 changed files with 489 additions and 204 deletions
|
@ -29,6 +29,9 @@ pub mod respawn;
|
||||||
pub mod send_client_end;
|
pub mod send_client_end;
|
||||||
pub mod task_pool;
|
pub mod task_pool;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod test_simulation;
|
||||||
|
|
||||||
pub use account::{Account, AccountOpts};
|
pub use account::{Account, AccountOpts};
|
||||||
pub use azalea_protocol::common::client_information::ClientInformation;
|
pub use azalea_protocol::common::client_information::ClientInformation;
|
||||||
pub use client::{
|
pub use client::{
|
||||||
|
|
|
@ -246,17 +246,10 @@ pub fn process_packet_events(ecs: &mut World) {
|
||||||
.insert(InstanceName(new_instance_name.clone()));
|
.insert(InstanceName(new_instance_name.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(dimension_type_element) =
|
let Some((_dimension_type, dimension_data)) = p
|
||||||
instance_holder.instance.read().registries.dimension_type()
|
.common
|
||||||
|
.dimension_type(&instance_holder.instance.read().registries)
|
||||||
else {
|
else {
|
||||||
error!("Server didn't send dimension type registry, can't log in");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let dimension_name = ResourceLocation::new(&p.common.dimension.to_string());
|
|
||||||
|
|
||||||
let Some(dimension) = dimension_type_element.map.get(&dimension_name) else {
|
|
||||||
error!("No dimension_type with name {dimension_name}");
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -264,8 +257,9 @@ pub fn process_packet_events(ecs: &mut World) {
|
||||||
// there)
|
// there)
|
||||||
let weak_instance = instance_container.insert(
|
let weak_instance = instance_container.insert(
|
||||||
new_instance_name.clone(),
|
new_instance_name.clone(),
|
||||||
dimension.height,
|
dimension_data.height,
|
||||||
dimension.min_y,
|
dimension_data.min_y,
|
||||||
|
&instance_holder.instance.read().registries,
|
||||||
);
|
);
|
||||||
instance_loaded_events.send(InstanceLoadedEvent {
|
instance_loaded_events.send(InstanceLoadedEvent {
|
||||||
entity: player_entity,
|
entity: player_entity,
|
||||||
|
@ -1387,17 +1381,10 @@ pub fn process_packet_events(ecs: &mut World) {
|
||||||
{
|
{
|
||||||
let new_instance_name = p.common.dimension.clone();
|
let new_instance_name = p.common.dimension.clone();
|
||||||
|
|
||||||
let Some(dimension_type_element) =
|
let Some((_dimension_type, dimension_data)) = p
|
||||||
instance_holder.instance.read().registries.dimension_type()
|
.common
|
||||||
|
.dimension_type(&instance_holder.instance.read().registries)
|
||||||
else {
|
else {
|
||||||
error!("Server didn't send dimension type registry, can't log in.");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let dimension_name = ResourceLocation::new(&p.common.dimension.to_string());
|
|
||||||
|
|
||||||
let Some(dimension) = dimension_type_element.map.get(&dimension_name) else {
|
|
||||||
error!("No dimension_type with name {dimension_name}");
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1405,8 +1392,9 @@ pub fn process_packet_events(ecs: &mut World) {
|
||||||
// there)
|
// there)
|
||||||
let weak_instance = instance_container.insert(
|
let weak_instance = instance_container.insert(
|
||||||
new_instance_name.clone(),
|
new_instance_name.clone(),
|
||||||
dimension.height,
|
dimension_data.height,
|
||||||
dimension.min_y,
|
dimension_data.min_y,
|
||||||
|
&instance_holder.instance.read().registries,
|
||||||
);
|
);
|
||||||
instance_loaded_events.send(InstanceLoadedEvent {
|
instance_loaded_events.send(InstanceLoadedEvent {
|
||||||
entity: player_entity,
|
entity: player_entity,
|
||||||
|
|
|
@ -1,102 +1,106 @@
|
||||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use azalea_auth::game_profile::GameProfile;
|
use azalea_auth::game_profile::GameProfile;
|
||||||
use azalea_client::{
|
use azalea_buf::AzaleaWrite;
|
||||||
|
use azalea_core::game_type::{GameMode, OptionalGameType};
|
||||||
|
use azalea_core::position::ChunkPos;
|
||||||
|
use azalea_core::resource_location::ResourceLocation;
|
||||||
|
use azalea_core::tick::GameTick;
|
||||||
|
use azalea_entity::metadata::PlayerMetadataBundle;
|
||||||
|
use azalea_protocol::packets::common::CommonPlayerSpawnInfo;
|
||||||
|
use azalea_protocol::packets::game::c_level_chunk_with_light::ClientboundLevelChunkPacketData;
|
||||||
|
use azalea_protocol::packets::game::c_light_update::ClientboundLightUpdatePacketData;
|
||||||
|
use azalea_protocol::packets::game::{
|
||||||
|
ClientboundLevelChunkWithLight, ClientboundLogin, ClientboundRespawn,
|
||||||
|
};
|
||||||
|
use azalea_protocol::packets::{ConnectionProtocol, Packet, ProtocolPacket};
|
||||||
|
use azalea_registry::DimensionType;
|
||||||
|
use azalea_world::palette::{PalettedContainer, PalettedContainerKind};
|
||||||
|
use azalea_world::{Chunk, Instance, MinecraftEntityId, Section};
|
||||||
|
use bevy_app::App;
|
||||||
|
use bevy_ecs::{prelude::*, schedule::ExecutorKind};
|
||||||
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
use simdnbt::owned::Nbt;
|
||||||
|
use tokio::{sync::mpsc, time::sleep};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
events::LocalPlayerEvents,
|
events::LocalPlayerEvents,
|
||||||
raw_connection::{RawConnection, RawConnectionReader, RawConnectionWriter},
|
raw_connection::{RawConnection, RawConnectionReader, RawConnectionWriter},
|
||||||
ClientInformation, GameProfileComponent, InConfigState, InstanceHolder, LocalPlayerBundle,
|
ClientInformation, GameProfileComponent, InConfigState, InstanceHolder, LocalPlayerBundle,
|
||||||
};
|
};
|
||||||
use azalea_core::{
|
|
||||||
game_type::{GameMode, OptionalGameType},
|
|
||||||
resource_location::ResourceLocation,
|
|
||||||
tick::GameTick,
|
|
||||||
};
|
|
||||||
use azalea_entity::{
|
|
||||||
metadata::{Health, PlayerMetadataBundle},
|
|
||||||
LocalEntity,
|
|
||||||
};
|
|
||||||
use azalea_protocol::packets::{
|
|
||||||
common::CommonPlayerSpawnInfo,
|
|
||||||
config::{ClientboundFinishConfiguration, ClientboundRegistryData},
|
|
||||||
game::{ClientboundLogin, ClientboundSetHealth},
|
|
||||||
ConnectionProtocol, Packet, ProtocolPacket,
|
|
||||||
};
|
|
||||||
use azalea_registry::DimensionType;
|
|
||||||
use azalea_world::{Instance, MinecraftEntityId};
|
|
||||||
use bevy_app::App;
|
|
||||||
use bevy_app::PluginGroup;
|
|
||||||
use bevy_ecs::{prelude::*, schedule::ExecutorKind};
|
|
||||||
use bevy_log::{tracing_subscriber, LogPlugin};
|
|
||||||
use parking_lot::{Mutex, RwLock};
|
|
||||||
use simdnbt::owned::{NbtCompound, NbtTag};
|
|
||||||
use tokio::{sync::mpsc, time::sleep};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[test]
|
/// A way to simulate a client in a server, used for some internal tests.
|
||||||
fn test_set_health_before_login() {
|
pub struct Simulation {
|
||||||
let _ = tracing_subscriber::fmt::try_init();
|
pub app: App,
|
||||||
|
pub entity: Entity,
|
||||||
|
|
||||||
let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
|
// the runtime needs to be kept around for the tasks to be considered alive
|
||||||
assert!(simulation.has_component::<InConfigState>());
|
pub rt: tokio::runtime::Runtime,
|
||||||
|
|
||||||
simulation.receive_packet(ClientboundRegistryData {
|
pub incoming_packet_queue: Arc<Mutex<Vec<Box<[u8]>>>>,
|
||||||
registry_id: ResourceLocation::new("minecraft:dimension_type"),
|
pub outgoing_packets_receiver: mpsc::UnboundedReceiver<Box<[u8]>>,
|
||||||
entries: vec![(
|
|
||||||
ResourceLocation::new("minecraft:overworld"),
|
|
||||||
Some(NbtCompound::from_values(vec![
|
|
||||||
("height".into(), NbtTag::Int(384)),
|
|
||||||
("min_y".into(), NbtTag::Int(-64)),
|
|
||||||
])),
|
|
||||||
)]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
});
|
|
||||||
simulation.tick();
|
|
||||||
simulation.receive_packet(ClientboundFinishConfiguration);
|
|
||||||
simulation.tick();
|
|
||||||
|
|
||||||
assert!(!simulation.has_component::<InConfigState>());
|
|
||||||
assert!(simulation.has_component::<LocalEntity>());
|
|
||||||
|
|
||||||
simulation.receive_packet(ClientboundSetHealth {
|
|
||||||
health: 15.,
|
|
||||||
food: 20,
|
|
||||||
saturation: 20.,
|
|
||||||
});
|
|
||||||
simulation.tick();
|
|
||||||
assert_eq!(*simulation.component::<Health>(), 15.);
|
|
||||||
|
|
||||||
simulation.receive_packet(ClientboundLogin {
|
|
||||||
player_id: MinecraftEntityId(0),
|
|
||||||
hardcore: false,
|
|
||||||
levels: vec![],
|
|
||||||
max_players: 20,
|
|
||||||
chunk_radius: 8,
|
|
||||||
simulation_distance: 8,
|
|
||||||
reduced_debug_info: false,
|
|
||||||
show_death_screen: true,
|
|
||||||
do_limited_crafting: false,
|
|
||||||
common: CommonPlayerSpawnInfo {
|
|
||||||
dimension_type: DimensionType::Overworld,
|
|
||||||
dimension: ResourceLocation::new("minecraft:overworld"),
|
|
||||||
seed: 0,
|
|
||||||
game_type: GameMode::Survival,
|
|
||||||
previous_game_type: OptionalGameType(None),
|
|
||||||
is_debug: false,
|
|
||||||
is_flat: false,
|
|
||||||
last_death_location: None,
|
|
||||||
portal_cooldown: 0,
|
|
||||||
sea_level: 63,
|
|
||||||
},
|
|
||||||
enforces_secure_chat: false,
|
|
||||||
});
|
|
||||||
simulation.tick();
|
|
||||||
|
|
||||||
// health should stay the same
|
|
||||||
assert_eq!(*simulation.component::<Health>(), 15.);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_local_player_bundle(
|
impl Simulation {
|
||||||
|
pub fn new(initial_connection_protocol: ConnectionProtocol) -> Self {
|
||||||
|
let mut app = create_simulation_app();
|
||||||
|
let mut entity = app.world_mut().spawn_empty();
|
||||||
|
let (player, outgoing_packets_receiver, incoming_packet_queue, rt) =
|
||||||
|
create_local_player_bundle(entity.id(), initial_connection_protocol);
|
||||||
|
entity.insert(player);
|
||||||
|
|
||||||
|
let entity = entity.id();
|
||||||
|
|
||||||
|
tick_app(&mut app);
|
||||||
|
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match initial_connection_protocol {
|
||||||
|
ConnectionProtocol::Configuration => {
|
||||||
|
app.world_mut().entity_mut(entity).insert(InConfigState);
|
||||||
|
tick_app(&mut app);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
app,
|
||||||
|
entity,
|
||||||
|
rt,
|
||||||
|
incoming_packet_queue,
|
||||||
|
outgoing_packets_receiver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receive_packet<P: ProtocolPacket + Debug>(&mut self, packet: impl Packet<P>) {
|
||||||
|
let buf = azalea_protocol::write::serialize_packet(&packet.into_variant()).unwrap();
|
||||||
|
self.incoming_packet_queue.lock().push(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self) {
|
||||||
|
tick_app(&mut self.app);
|
||||||
|
}
|
||||||
|
pub fn component<T: Component + Clone>(&self) -> T {
|
||||||
|
self.app.world().get::<T>(self.entity).unwrap().clone()
|
||||||
|
}
|
||||||
|
pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
|
||||||
|
self.app.world().get::<T>(self.entity).cloned()
|
||||||
|
}
|
||||||
|
pub fn has_component<T: Component>(&self) -> bool {
|
||||||
|
self.app.world().get::<T>(self.entity).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chunk(&self, chunk_pos: ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
|
||||||
|
self.component::<InstanceHolder>()
|
||||||
|
.instance
|
||||||
|
.read()
|
||||||
|
.chunks
|
||||||
|
.get(&chunk_pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn create_local_player_bundle(
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
connection_protocol: ConnectionProtocol,
|
connection_protocol: ConnectionProtocol,
|
||||||
) -> (
|
) -> (
|
||||||
|
@ -164,7 +168,12 @@ pub fn create_local_player_bundle(
|
||||||
|
|
||||||
fn create_simulation_app() -> App {
|
fn create_simulation_app() -> App {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
app.add_plugins(azalea_client::DefaultPlugins.build().disable::<LogPlugin>());
|
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
app.add_plugins(
|
||||||
|
bevy_app::PluginGroup::build(crate::DefaultPlugins).disable::<bevy_log::LogPlugin>(),
|
||||||
|
);
|
||||||
|
|
||||||
app.edit_schedule(bevy_app::Main, |schedule| {
|
app.edit_schedule(bevy_app::Main, |schedule| {
|
||||||
// makes test results more reproducible
|
// makes test results more reproducible
|
||||||
schedule.set_executor_kind(ExecutorKind::SingleThreaded);
|
schedule.set_executor_kind(ExecutorKind::SingleThreaded);
|
||||||
|
@ -172,66 +181,85 @@ fn create_simulation_app() -> App {
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Simulation {
|
|
||||||
pub app: App,
|
|
||||||
pub entity: Entity,
|
|
||||||
|
|
||||||
// the runtime needs to be kept around for the tasks to be considered alive
|
|
||||||
pub rt: tokio::runtime::Runtime,
|
|
||||||
|
|
||||||
pub incoming_packet_queue: Arc<Mutex<Vec<Box<[u8]>>>>,
|
|
||||||
pub outgoing_packets_receiver: mpsc::UnboundedReceiver<Box<[u8]>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Simulation {
|
|
||||||
pub fn new(initial_connection_protocol: ConnectionProtocol) -> Self {
|
|
||||||
let mut app = create_simulation_app();
|
|
||||||
let mut entity = app.world_mut().spawn_empty();
|
|
||||||
let (player, outgoing_packets_receiver, incoming_packet_queue, rt) =
|
|
||||||
create_local_player_bundle(entity.id(), initial_connection_protocol);
|
|
||||||
entity.insert(player);
|
|
||||||
|
|
||||||
let entity = entity.id();
|
|
||||||
|
|
||||||
tick_app(&mut app);
|
|
||||||
|
|
||||||
match initial_connection_protocol {
|
|
||||||
ConnectionProtocol::Configuration => {
|
|
||||||
app.world_mut().entity_mut(entity).insert(InConfigState);
|
|
||||||
tick_app(&mut app);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
app,
|
|
||||||
entity,
|
|
||||||
rt,
|
|
||||||
incoming_packet_queue,
|
|
||||||
outgoing_packets_receiver,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn receive_packet<P: ProtocolPacket + Debug>(&mut self, packet: impl Packet<P>) {
|
|
||||||
let buf = azalea_protocol::write::serialize_packet(&packet.into_variant()).unwrap();
|
|
||||||
self.incoming_packet_queue.lock().push(buf.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(&mut self) {
|
|
||||||
tick_app(&mut self.app);
|
|
||||||
}
|
|
||||||
pub fn component<T: Component + Clone>(&self) -> T {
|
|
||||||
self.app.world().get::<T>(self.entity).unwrap().clone()
|
|
||||||
}
|
|
||||||
pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
|
|
||||||
self.app.world().get::<T>(self.entity).cloned()
|
|
||||||
}
|
|
||||||
pub fn has_component<T: Component>(&self) -> bool {
|
|
||||||
self.app.world().get::<T>(self.entity).is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tick_app(app: &mut App) {
|
fn tick_app(app: &mut App) {
|
||||||
app.update();
|
app.update();
|
||||||
app.world_mut().run_schedule(GameTick);
|
app.world_mut().run_schedule(GameTick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn make_basic_login_packet(
|
||||||
|
dimension_type: DimensionType,
|
||||||
|
dimension: ResourceLocation,
|
||||||
|
) -> ClientboundLogin {
|
||||||
|
ClientboundLogin {
|
||||||
|
player_id: MinecraftEntityId(0),
|
||||||
|
hardcore: false,
|
||||||
|
levels: vec![],
|
||||||
|
max_players: 20,
|
||||||
|
chunk_radius: 8,
|
||||||
|
simulation_distance: 8,
|
||||||
|
reduced_debug_info: false,
|
||||||
|
show_death_screen: true,
|
||||||
|
do_limited_crafting: false,
|
||||||
|
common: CommonPlayerSpawnInfo {
|
||||||
|
dimension_type,
|
||||||
|
dimension,
|
||||||
|
seed: 0,
|
||||||
|
game_type: GameMode::Survival,
|
||||||
|
previous_game_type: OptionalGameType(None),
|
||||||
|
is_debug: false,
|
||||||
|
is_flat: false,
|
||||||
|
last_death_location: None,
|
||||||
|
portal_cooldown: 0,
|
||||||
|
sea_level: 63,
|
||||||
|
},
|
||||||
|
enforces_secure_chat: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_basic_respawn_packet(
|
||||||
|
dimension_type: DimensionType,
|
||||||
|
dimension: ResourceLocation,
|
||||||
|
) -> ClientboundRespawn {
|
||||||
|
ClientboundRespawn {
|
||||||
|
common: CommonPlayerSpawnInfo {
|
||||||
|
dimension_type,
|
||||||
|
dimension,
|
||||||
|
seed: 0,
|
||||||
|
game_type: GameMode::Survival,
|
||||||
|
previous_game_type: OptionalGameType(None),
|
||||||
|
is_debug: false,
|
||||||
|
is_flat: false,
|
||||||
|
last_death_location: None,
|
||||||
|
portal_cooldown: 0,
|
||||||
|
sea_level: 63,
|
||||||
|
},
|
||||||
|
data_to_keep: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_basic_empty_chunk(
|
||||||
|
pos: ChunkPos,
|
||||||
|
section_count: usize,
|
||||||
|
) -> ClientboundLevelChunkWithLight {
|
||||||
|
let mut chunk_bytes = Vec::new();
|
||||||
|
let mut sections = Vec::new();
|
||||||
|
for _ in 0..section_count {
|
||||||
|
sections.push(Section {
|
||||||
|
block_count: 0,
|
||||||
|
states: PalettedContainer::new(PalettedContainerKind::BlockStates),
|
||||||
|
biomes: PalettedContainer::new(PalettedContainerKind::Biomes),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sections.azalea_write(&mut chunk_bytes).unwrap();
|
||||||
|
|
||||||
|
ClientboundLevelChunkWithLight {
|
||||||
|
x: pos.x,
|
||||||
|
z: pos.z,
|
||||||
|
chunk_data: ClientboundLevelChunkPacketData {
|
||||||
|
heightmaps: Nbt::None,
|
||||||
|
data: chunk_bytes,
|
||||||
|
block_entities: vec![],
|
||||||
|
},
|
||||||
|
light_data: ClientboundLightUpdatePacketData::default(),
|
||||||
|
}
|
||||||
|
}
|
147
azalea-client/tests/change_dimension_to_nether_and_back.rs
Normal file
147
azalea-client/tests/change_dimension_to_nether_and_back.rs
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
use azalea_client::{test_simulation::*, InConfigState};
|
||||||
|
use azalea_core::{position::ChunkPos, resource_location::ResourceLocation};
|
||||||
|
use azalea_entity::{metadata::Health, LocalEntity};
|
||||||
|
use azalea_protocol::packets::{
|
||||||
|
config::{ClientboundFinishConfiguration, ClientboundRegistryData},
|
||||||
|
game::ClientboundSetHealth,
|
||||||
|
ConnectionProtocol,
|
||||||
|
};
|
||||||
|
use azalea_registry::DimensionType;
|
||||||
|
use azalea_world::InstanceName;
|
||||||
|
use bevy_log::tracing_subscriber;
|
||||||
|
use simdnbt::owned::{NbtCompound, NbtTag};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_change_dimension_to_nether_and_back() {
|
||||||
|
let _ = tracing_subscriber::fmt::try_init();
|
||||||
|
|
||||||
|
let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
|
||||||
|
assert!(simulation.has_component::<InConfigState>());
|
||||||
|
|
||||||
|
simulation.receive_packet(ClientboundRegistryData {
|
||||||
|
registry_id: ResourceLocation::new("minecraft:dimension_type"),
|
||||||
|
entries: vec![
|
||||||
|
(
|
||||||
|
// this dimension should never be created. it just exists to make sure we're not
|
||||||
|
// hard-coding the dimension type id anywhere.
|
||||||
|
ResourceLocation::new("azalea:fakedimension"),
|
||||||
|
Some(NbtCompound::from_values(vec![
|
||||||
|
("height".into(), NbtTag::Int(16)),
|
||||||
|
("min_y".into(), NbtTag::Int(0)),
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ResourceLocation::new("minecraft:overworld"),
|
||||||
|
Some(NbtCompound::from_values(vec![
|
||||||
|
("height".into(), NbtTag::Int(384)),
|
||||||
|
("min_y".into(), NbtTag::Int(-64)),
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ResourceLocation::new("minecraft:nether"),
|
||||||
|
Some(NbtCompound::from_values(vec![
|
||||||
|
("height".into(), NbtTag::Int(256)),
|
||||||
|
("min_y".into(), NbtTag::Int(0)),
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
simulation.tick();
|
||||||
|
simulation.receive_packet(ClientboundFinishConfiguration);
|
||||||
|
simulation.tick();
|
||||||
|
|
||||||
|
assert!(!simulation.has_component::<InConfigState>());
|
||||||
|
assert!(simulation.has_component::<LocalEntity>());
|
||||||
|
|
||||||
|
simulation.receive_packet(ClientboundSetHealth {
|
||||||
|
health: 15.,
|
||||||
|
food: 20,
|
||||||
|
saturation: 20.,
|
||||||
|
});
|
||||||
|
simulation.tick();
|
||||||
|
assert_eq!(*simulation.component::<Health>(), 15.);
|
||||||
|
|
||||||
|
//
|
||||||
|
// OVERWORLD
|
||||||
|
//
|
||||||
|
|
||||||
|
simulation.receive_packet(make_basic_login_packet(
|
||||||
|
DimensionType::new_raw(1), // overworld
|
||||||
|
ResourceLocation::new("azalea:a"),
|
||||||
|
));
|
||||||
|
simulation.tick();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*simulation.component::<InstanceName>(),
|
||||||
|
ResourceLocation::new("azalea:a"),
|
||||||
|
"InstanceName should be azalea:a after setting dimension to that"
|
||||||
|
);
|
||||||
|
|
||||||
|
simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
|
||||||
|
simulation.tick();
|
||||||
|
// make sure the chunk exists
|
||||||
|
simulation
|
||||||
|
.chunk(ChunkPos::new(0, 0))
|
||||||
|
.expect("chunk should exist");
|
||||||
|
|
||||||
|
//
|
||||||
|
// NETHER
|
||||||
|
//
|
||||||
|
|
||||||
|
simulation.receive_packet(make_basic_respawn_packet(
|
||||||
|
DimensionType::new_raw(2), // nether
|
||||||
|
ResourceLocation::new("azalea:b"),
|
||||||
|
));
|
||||||
|
simulation.tick();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
simulation.chunk(ChunkPos::new(0, 0)).is_none(),
|
||||||
|
"chunk should not exist immediately after changing dimensions"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*simulation.component::<InstanceName>(),
|
||||||
|
ResourceLocation::new("azalea:b"),
|
||||||
|
"InstanceName should be azalea:b after changing dimensions to that"
|
||||||
|
);
|
||||||
|
|
||||||
|
simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), 256 / 16));
|
||||||
|
simulation.tick();
|
||||||
|
// make sure the chunk exists
|
||||||
|
simulation
|
||||||
|
.chunk(ChunkPos::new(0, 0))
|
||||||
|
.expect("chunk should exist");
|
||||||
|
simulation.receive_packet(make_basic_respawn_packet(
|
||||||
|
DimensionType::new_raw(2), // nether
|
||||||
|
ResourceLocation::new("minecraft:nether"),
|
||||||
|
));
|
||||||
|
simulation.tick();
|
||||||
|
|
||||||
|
//
|
||||||
|
// BACK TO OVERWORLD
|
||||||
|
//
|
||||||
|
|
||||||
|
simulation.receive_packet(make_basic_login_packet(
|
||||||
|
DimensionType::new_raw(1), // overworld
|
||||||
|
ResourceLocation::new("azalea:a"),
|
||||||
|
));
|
||||||
|
simulation.tick();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*simulation.component::<InstanceName>(),
|
||||||
|
ResourceLocation::new("azalea:a"),
|
||||||
|
"InstanceName should be azalea:a after setting dimension back to that"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
simulation.chunk(ChunkPos::new(0, 0)).is_none(),
|
||||||
|
"chunk should not exist immediately after switching back to overworld"
|
||||||
|
);
|
||||||
|
|
||||||
|
simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
|
||||||
|
simulation.tick();
|
||||||
|
// make sure the chunk exists
|
||||||
|
simulation
|
||||||
|
.chunk(ChunkPos::new(0, 0))
|
||||||
|
.expect("chunk should exist");
|
||||||
|
}
|
55
azalea-client/tests/set_health_before_login.rs
Normal file
55
azalea-client/tests/set_health_before_login.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use azalea_client::{test_simulation::*, InConfigState};
|
||||||
|
use azalea_core::resource_location::ResourceLocation;
|
||||||
|
use azalea_entity::{metadata::Health, LocalEntity};
|
||||||
|
use azalea_protocol::packets::{
|
||||||
|
config::{ClientboundFinishConfiguration, ClientboundRegistryData},
|
||||||
|
game::ClientboundSetHealth,
|
||||||
|
ConnectionProtocol,
|
||||||
|
};
|
||||||
|
use azalea_registry::DimensionType;
|
||||||
|
use bevy_log::tracing_subscriber;
|
||||||
|
use simdnbt::owned::{NbtCompound, NbtTag};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_health_before_login() {
|
||||||
|
let _ = tracing_subscriber::fmt::try_init();
|
||||||
|
|
||||||
|
let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
|
||||||
|
assert!(simulation.has_component::<InConfigState>());
|
||||||
|
|
||||||
|
simulation.receive_packet(ClientboundRegistryData {
|
||||||
|
registry_id: ResourceLocation::new("minecraft:dimension_type"),
|
||||||
|
entries: vec![(
|
||||||
|
ResourceLocation::new("minecraft:overworld"),
|
||||||
|
Some(NbtCompound::from_values(vec![
|
||||||
|
("height".into(), NbtTag::Int(384)),
|
||||||
|
("min_y".into(), NbtTag::Int(-64)),
|
||||||
|
])),
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
simulation.tick();
|
||||||
|
simulation.receive_packet(ClientboundFinishConfiguration);
|
||||||
|
simulation.tick();
|
||||||
|
|
||||||
|
assert!(!simulation.has_component::<InConfigState>());
|
||||||
|
assert!(simulation.has_component::<LocalEntity>());
|
||||||
|
|
||||||
|
simulation.receive_packet(ClientboundSetHealth {
|
||||||
|
health: 15.,
|
||||||
|
food: 20,
|
||||||
|
saturation: 20.,
|
||||||
|
});
|
||||||
|
simulation.tick();
|
||||||
|
assert_eq!(*simulation.component::<Health>(), 15.);
|
||||||
|
|
||||||
|
simulation.receive_packet(make_basic_login_packet(
|
||||||
|
DimensionType::new_raw(0), // overworld
|
||||||
|
ResourceLocation::new("minecraft:overworld"),
|
||||||
|
));
|
||||||
|
simulation.tick();
|
||||||
|
|
||||||
|
// health should stay the same
|
||||||
|
assert_eq!(*simulation.component::<Health>(), 15.);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use std::str::FromStr;
|
use std::{io::Cursor, str::FromStr};
|
||||||
|
|
||||||
use azalea_registry::DataRegistry;
|
use azalea_registry::DataRegistry;
|
||||||
use simdnbt::owned::NbtCompound;
|
use simdnbt::owned::NbtCompound;
|
||||||
|
@ -23,5 +23,25 @@ pub trait ResolvableDataRegistry: DataRegistry {
|
||||||
let resolved = registry_values.get_index(self.protocol_id() as usize)?;
|
let resolved = registry_values.get_index(self.protocol_id() as usize)?;
|
||||||
Some(resolved)
|
Some(resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_and_deserialize<T: simdnbt::Deserialize>(
|
||||||
|
&self,
|
||||||
|
registries: &RegistryHolder,
|
||||||
|
) -> Option<Result<(ResourceLocation, T), simdnbt::DeserializeError>> {
|
||||||
|
let (name, value) = self.resolve(registries)?;
|
||||||
|
|
||||||
|
let mut nbt_bytes = Vec::new();
|
||||||
|
value.write(&mut nbt_bytes);
|
||||||
|
let nbt_borrow_compound =
|
||||||
|
simdnbt::borrow::read_compound(&mut Cursor::new(&nbt_bytes)).ok()?;
|
||||||
|
let value = match T::from_compound((&nbt_borrow_compound).into()) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
return Some(Err(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Ok((name.clone(), value)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl<T: DataRegistry> ResolvableDataRegistry for T {}
|
impl<T: DataRegistry> ResolvableDataRegistry for T {}
|
||||||
|
|
|
@ -19,6 +19,11 @@ use crate::resource_location::ResourceLocation;
|
||||||
/// The base of the registry.
|
/// The base of the registry.
|
||||||
///
|
///
|
||||||
/// This is the registry that is sent to the client upon login.
|
/// This is the registry that is sent to the client upon login.
|
||||||
|
///
|
||||||
|
/// Note that `azalea-client` stores registries per-world instead of per-client
|
||||||
|
/// like you might expect. This is an optimization for swarms to reduce memory
|
||||||
|
/// usage, since registries are expected to be the same for every client in a
|
||||||
|
/// world.
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct RegistryHolder {
|
pub struct RegistryHolder {
|
||||||
pub map: HashMap<ResourceLocation, IndexMap<ResourceLocation, NbtCompound>>,
|
pub map: HashMap<ResourceLocation, IndexMap<ResourceLocation, NbtCompound>>,
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use azalea_core::{
|
use azalea_core::{
|
||||||
position::{BlockPos, ChunkPos, Vec3},
|
position::{BlockPos, ChunkPos, Vec3},
|
||||||
|
registry_holder::RegistryHolder,
|
||||||
resource_location::ResourceLocation,
|
resource_location::ResourceLocation,
|
||||||
tick::GameTick,
|
tick::GameTick,
|
||||||
};
|
};
|
||||||
use azalea_entity::{EntityBundle, EntityPlugin, LocalEntity, Physics, Position};
|
use azalea_entity::{EntityBundle, EntityPlugin, LocalEntity, Physics, Position};
|
||||||
use azalea_physics::PhysicsPlugin;
|
use azalea_physics::PhysicsPlugin;
|
||||||
use azalea_world::{Chunk, InstanceContainer, MinecraftEntityId, PartialInstance};
|
use azalea_world::{Chunk, Instance, InstanceContainer, MinecraftEntityId, PartialInstance};
|
||||||
use bevy_app::App;
|
use bevy_app::App;
|
||||||
|
use parking_lot::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// You need an app to spawn entities in the world and do updates.
|
/// You need an app to spawn entities in the world and do updates.
|
||||||
|
@ -17,14 +21,19 @@ fn make_test_app() -> App {
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
pub fn insert_overworld(app: &mut App) -> Arc<RwLock<Instance>> {
|
||||||
fn test_gravity() {
|
app.world_mut().resource_mut::<InstanceContainer>().insert(
|
||||||
let mut app = make_test_app();
|
|
||||||
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
|
|
||||||
ResourceLocation::new("minecraft:overworld"),
|
ResourceLocation::new("minecraft:overworld"),
|
||||||
384,
|
384,
|
||||||
-64,
|
-64,
|
||||||
);
|
&RegistryHolder::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gravity() {
|
||||||
|
let mut app = make_test_app();
|
||||||
|
let world_lock = insert_overworld(&mut app);
|
||||||
let mut partial_world = PartialInstance::default();
|
let mut partial_world = PartialInstance::default();
|
||||||
// the entity has to be in a loaded chunk for physics to work
|
// the entity has to be in a loaded chunk for physics to work
|
||||||
partial_world.chunks.set(
|
partial_world.chunks.set(
|
||||||
|
@ -80,11 +89,7 @@ fn test_gravity() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_collision() {
|
fn test_collision() {
|
||||||
let mut app = make_test_app();
|
let mut app = make_test_app();
|
||||||
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
|
let world_lock = insert_overworld(&mut app);
|
||||||
ResourceLocation::new("minecraft:overworld"),
|
|
||||||
384,
|
|
||||||
-64,
|
|
||||||
);
|
|
||||||
let mut partial_world = PartialInstance::default();
|
let mut partial_world = PartialInstance::default();
|
||||||
|
|
||||||
partial_world.chunks.set(
|
partial_world.chunks.set(
|
||||||
|
@ -140,11 +145,7 @@ fn test_collision() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_slab_collision() {
|
fn test_slab_collision() {
|
||||||
let mut app = make_test_app();
|
let mut app = make_test_app();
|
||||||
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
|
let world_lock = insert_overworld(&mut app);
|
||||||
ResourceLocation::new("minecraft:overworld"),
|
|
||||||
384,
|
|
||||||
-64,
|
|
||||||
);
|
|
||||||
let mut partial_world = PartialInstance::default();
|
let mut partial_world = PartialInstance::default();
|
||||||
|
|
||||||
partial_world.chunks.set(
|
partial_world.chunks.set(
|
||||||
|
@ -194,11 +195,7 @@ fn test_slab_collision() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_top_slab_collision() {
|
fn test_top_slab_collision() {
|
||||||
let mut app = make_test_app();
|
let mut app = make_test_app();
|
||||||
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
|
let world_lock = insert_overworld(&mut app);
|
||||||
ResourceLocation::new("minecraft:overworld"),
|
|
||||||
384,
|
|
||||||
-64,
|
|
||||||
);
|
|
||||||
let mut partial_world = PartialInstance::default();
|
let mut partial_world = PartialInstance::default();
|
||||||
|
|
||||||
partial_world.chunks.set(
|
partial_world.chunks.set(
|
||||||
|
@ -251,6 +248,7 @@ fn test_weird_wall_collision() {
|
||||||
ResourceLocation::new("minecraft:overworld"),
|
ResourceLocation::new("minecraft:overworld"),
|
||||||
384,
|
384,
|
||||||
-64,
|
-64,
|
||||||
|
&RegistryHolder::default(),
|
||||||
);
|
);
|
||||||
let mut partial_world = PartialInstance::default();
|
let mut partial_world = PartialInstance::default();
|
||||||
|
|
||||||
|
@ -309,6 +307,7 @@ fn test_negative_coordinates_weird_wall_collision() {
|
||||||
ResourceLocation::new("minecraft:overworld"),
|
ResourceLocation::new("minecraft:overworld"),
|
||||||
384,
|
384,
|
||||||
-64,
|
-64,
|
||||||
|
&RegistryHolder::default(),
|
||||||
);
|
);
|
||||||
let mut partial_world = PartialInstance::default();
|
let mut partial_world = PartialInstance::default();
|
||||||
|
|
||||||
|
@ -371,6 +370,7 @@ fn spawn_and_unload_world() {
|
||||||
ResourceLocation::new("minecraft:overworld"),
|
ResourceLocation::new("minecraft:overworld"),
|
||||||
384,
|
384,
|
||||||
-64,
|
-64,
|
||||||
|
&RegistryHolder::default(),
|
||||||
);
|
);
|
||||||
let mut partial_world = PartialInstance::default();
|
let mut partial_world = PartialInstance::default();
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
use azalea_buf::AzBuf;
|
use azalea_buf::AzBuf;
|
||||||
use azalea_core::{
|
use azalea_core::{
|
||||||
|
data_registry::ResolvableDataRegistry,
|
||||||
game_type::{GameMode, OptionalGameType},
|
game_type::{GameMode, OptionalGameType},
|
||||||
position::GlobalPos,
|
position::GlobalPos,
|
||||||
|
registry_holder::{DimensionTypeElement, RegistryHolder},
|
||||||
resource_location::ResourceLocation,
|
resource_location::ResourceLocation,
|
||||||
};
|
};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Clone, Debug, AzBuf)]
|
#[derive(Clone, Debug, AzBuf)]
|
||||||
pub struct CommonPlayerSpawnInfo {
|
pub struct CommonPlayerSpawnInfo {
|
||||||
|
@ -20,3 +23,29 @@ pub struct CommonPlayerSpawnInfo {
|
||||||
#[var]
|
#[var]
|
||||||
pub sea_level: i32,
|
pub sea_level: i32,
|
||||||
}
|
}
|
||||||
|
impl CommonPlayerSpawnInfo {
|
||||||
|
pub fn dimension_type(
|
||||||
|
&self,
|
||||||
|
registry_holder: &RegistryHolder,
|
||||||
|
) -> Option<(ResourceLocation, DimensionTypeElement)> {
|
||||||
|
let dimension_res = self
|
||||||
|
.dimension_type
|
||||||
|
.resolve_and_deserialize::<DimensionTypeElement>(registry_holder);
|
||||||
|
let Some(dimension_res) = dimension_res else {
|
||||||
|
error!("Couldn't resolve dimension_type {:?}", self.dimension_type);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let (dimension_type, dimension_data) = match dimension_res {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"Couldn't deserialize dimension_type {:?}: {err:?}",
|
||||||
|
self.dimension_type
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((dimension_type, dimension_data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use super::c_light_update::ClientboundLightUpdatePacketData;
|
||||||
|
|
||||||
#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)]
|
#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)]
|
||||||
pub struct ClientboundLevelChunkWithLight {
|
pub struct ClientboundLevelChunkWithLight {
|
||||||
|
// this can't be a ChunkPos since that reads z first and then x
|
||||||
pub x: i32,
|
pub x: i32,
|
||||||
pub z: i32,
|
pub z: i32,
|
||||||
pub chunk_data: ClientboundLevelChunkPacketData,
|
pub chunk_data: ClientboundLevelChunkPacketData,
|
||||||
|
|
|
@ -11,7 +11,7 @@ pub struct ClientboundLightUpdate {
|
||||||
pub light_data: ClientboundLightUpdatePacketData,
|
pub light_data: ClientboundLightUpdatePacketData,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, AzBuf)]
|
#[derive(Clone, Debug, AzBuf, Default)]
|
||||||
pub struct ClientboundLightUpdatePacketData {
|
pub struct ClientboundLightUpdatePacketData {
|
||||||
pub sky_y_mask: BitSet,
|
pub sky_y_mask: BitSet,
|
||||||
pub block_y_mask: BitSet,
|
pub block_y_mask: BitSet,
|
||||||
|
|
|
@ -22,3 +22,20 @@ impl DataRegistry for Enchantment {
|
||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, AzBuf, PartialEq, Eq, Hash)]
|
||||||
|
pub struct DimensionType {
|
||||||
|
#[var]
|
||||||
|
id: u32,
|
||||||
|
}
|
||||||
|
impl DimensionType {
|
||||||
|
pub fn new_raw(id: u32) -> Self {
|
||||||
|
Self { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DataRegistry for DimensionType {
|
||||||
|
const NAME: &'static str = "dimension_type";
|
||||||
|
fn protocol_id(&self) -> u32 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,15 +27,6 @@ impl Default for WolfVariant {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registry! {
|
|
||||||
enum DimensionType {
|
|
||||||
Overworld => "minecraft:overworld",
|
|
||||||
Nether => "minecraft:the_nether",
|
|
||||||
End => "minecraft:the_end",
|
|
||||||
OverworldCaves => "minecraft:overworld_caves",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registry! {
|
registry! {
|
||||||
enum TrimMaterial {
|
enum TrimMaterial {
|
||||||
Quartz => "minecraft:quartz",
|
Quartz => "minecraft:quartz",
|
||||||
|
|
|
@ -51,6 +51,7 @@ impl InstanceContainer {
|
||||||
name: ResourceLocation,
|
name: ResourceLocation,
|
||||||
height: u32,
|
height: u32,
|
||||||
min_y: i32,
|
min_y: i32,
|
||||||
|
default_registries: &RegistryHolder,
|
||||||
) -> Arc<RwLock<Instance>> {
|
) -> Arc<RwLock<Instance>> {
|
||||||
if let Some(existing_lock) = self.instances.get(&name).and_then(|world| world.upgrade()) {
|
if let Some(existing_lock) = self.instances.get(&name).and_then(|world| world.upgrade()) {
|
||||||
let existing = existing_lock.read();
|
let existing = existing_lock.read();
|
||||||
|
@ -72,7 +73,7 @@ impl InstanceContainer {
|
||||||
chunks: ChunkStorage::new(height, min_y),
|
chunks: ChunkStorage::new(height, min_y),
|
||||||
entities_by_chunk: HashMap::new(),
|
entities_by_chunk: HashMap::new(),
|
||||||
entity_by_id: IntMap::default(),
|
entity_by_id: IntMap::default(),
|
||||||
registries: RegistryHolder::default(),
|
registries: default_registries.clone(),
|
||||||
}));
|
}));
|
||||||
debug!("Added new instance {name}");
|
debug!("Added new instance {name}");
|
||||||
self.instances.insert(name, Arc::downgrade(&world));
|
self.instances.insert(name, Arc::downgrade(&world));
|
||||||
|
|
Loading…
Add table
Reference in a new issue