aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock3
-rwxr-xr-xazalea-buf/src/read.rs11
-rw-r--r--azalea-client/src/chunks.rs34
-rw-r--r--azalea-client/src/client.rs14
-rw-r--r--azalea-client/src/events.rs2
-rw-r--r--azalea-client/src/lib.rs2
-rw-r--r--azalea-client/src/local_player.rs2
-rw-r--r--azalea-client/src/packet_handling/configuration.rs29
-rw-r--r--azalea-client/src/packet_handling/game.rs70
-rw-r--r--azalea-client/src/received_registries.rs28
-rw-r--r--azalea-core/Cargo.toml1
-rwxr-xr-xazalea-core/src/lib.rs1
-rw-r--r--azalea-core/src/registry_holder.rs412
-rwxr-xr-xazalea-physics/src/collision/shape.rs23
-rw-r--r--azalea-protocol/src/packets/configuration/clientbound_registry_data_packet.rs402
-rw-r--r--azalea-world/Cargo.toml2
-rwxr-xr-xazalea-world/src/chunk_storage.rs91
-rw-r--r--azalea-world/src/container.rs3
-rw-r--r--azalea-world/src/world.rs4
-rw-r--r--azalea/examples/testbot.rs48
-rw-r--r--azalea/src/swarm/mod.rs8
21 files changed, 634 insertions, 556 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5f769809..2331c5ee 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -332,6 +332,7 @@ dependencies = [
"nohash-hasher",
"num-traits",
"serde",
+ "serde_json",
"uuid",
]
@@ -525,6 +526,8 @@ dependencies = [
"nohash-hasher",
"once_cell",
"parking_lot",
+ "serde",
+ "serde_json",
"thiserror",
"tracing",
"uuid",
diff --git a/azalea-buf/src/read.rs b/azalea-buf/src/read.rs
index b4b54917..78db7357 100755
--- a/azalea-buf/src/read.rs
+++ b/azalea-buf/src/read.rs
@@ -173,9 +173,8 @@ impl McBufReadable for UnsizedByteArray {
impl<T: McBufReadable + Send> McBufReadable for Vec<T> {
default fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let length = u32::var_read_from(buf)? as usize;
- // we don't set the capacity here so we can't get exploited into
- // allocating a bunch
- let mut contents = Vec::new();
+ // we limit the capacity to not get exploited into allocating a bunch
+ let mut contents = Vec::with_capacity(usize::min(length, 65536));
for _ in 0..length {
contents.push(T::read_from(buf)?);
}
@@ -186,7 +185,7 @@ impl<T: McBufReadable + Send> McBufReadable for Vec<T> {
impl<K: McBufReadable + Send + Eq + Hash, V: McBufReadable + Send> McBufReadable for HashMap<K, V> {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let length = i32::var_read_from(buf)? as usize;
- let mut contents = HashMap::new();
+ let mut contents = HashMap::with_capacity(usize::min(length, 65536));
for _ in 0..length {
contents.insert(K::read_from(buf)?, V::read_from(buf)?);
}
@@ -199,7 +198,7 @@ impl<K: McBufReadable + Send + Eq + Hash, V: McBufVarReadable + Send> McBufVarRe
{
fn var_read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let length = i32::var_read_from(buf)? as usize;
- let mut contents = HashMap::new();
+ let mut contents = HashMap::with_capacity(usize::min(length, 65536));
for _ in 0..length {
contents.insert(K::read_from(buf)?, V::var_read_from(buf)?);
}
@@ -253,7 +252,7 @@ impl McBufVarReadable for u16 {
impl<T: McBufVarReadable> McBufVarReadable for Vec<T> {
fn var_read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let length = i32::var_read_from(buf)? as usize;
- let mut contents = Vec::new();
+ let mut contents = Vec::with_capacity(usize::min(length, 65536));
for _ in 0..length {
contents.push(T::var_read_from(buf)?);
}
diff --git a/azalea-client/src/chunks.rs b/azalea-client/src/chunks.rs
index 80c350c8..4d2641f5 100644
--- a/azalea-client/src/chunks.rs
+++ b/azalea-client/src/chunks.rs
@@ -79,30 +79,22 @@ fn handle_receive_chunk_events(
let local_player = query.get_mut(event.entity).unwrap();
- // OPTIMIZATION: if we already know about the chunk from the
- // shared world (and not ourselves), then we don't need to
- // parse it again. This is only used when we have a shared
- // world, since we check that the chunk isn't currently owned
- // by this client.
- let shared_chunk = local_player.instance.read().chunks.get(&pos);
- let this_client_has_chunk = local_player
- .partial_instance
- .read()
- .chunks
- .limited_get(&pos)
- .is_some();
+ let mut instance = local_player.instance.write();
+ let mut partial_instance = local_player.partial_instance.write();
- let mut world = local_player.instance.write();
- let mut partial_world = local_player.partial_instance.write();
+ // OPTIMIZATION: if we already know about the chunk from the shared world (and
+ // not ourselves), then we don't need to parse it again. This is only used when
+ // we have a shared world, since we check that the chunk isn't currently owned
+ // by this client.
+ let shared_chunk = instance.chunks.get(&pos);
+ let this_client_has_chunk = partial_instance.chunks.limited_get(&pos).is_some();
if !this_client_has_chunk {
if let Some(shared_chunk) = shared_chunk {
trace!("Skipping parsing chunk {pos:?} because we already know about it");
- partial_world.chunks.set_with_shared_reference(
- &pos,
- Some(shared_chunk.clone()),
- &mut world.chunks,
- );
+ partial_instance
+ .chunks
+ .limited_set(&pos, Some(shared_chunk));
continue;
}
}
@@ -112,11 +104,11 @@ fn handle_receive_chunk_events(
let empty_nbt_compound = NbtCompound::default();
let heightmaps = heightmaps.unwrap_or(&empty_nbt_compound);
- if let Err(e) = partial_world.chunks.replace_with_packet_data(
+ if let Err(e) = partial_instance.chunks.replace_with_packet_data(
&pos,
&mut Cursor::new(&event.packet.chunk_data.data),
heightmaps,
- &mut world.chunks,
+ &mut instance.chunks,
) {
error!("Couldn't set chunk data: {e}");
}
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index 4f1c4b9e..787e6144 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -17,7 +17,7 @@ use crate::{
raw_connection::RawConnection,
respawn::RespawnPlugin,
task_pool::TaskPoolPlugin,
- Account, PlayerInfo, ReceivedRegistries,
+ Account, PlayerInfo,
};
use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
@@ -99,8 +99,6 @@ pub struct Client {
pub profile: GameProfile,
/// The entity for this client in the ECS.
pub entity: Entity,
- /// The world that this client is in.
- pub world: Arc<RwLock<PartialInstance>>,
/// The entity component system. You probably don't need to access this
/// directly. Note that if you're using a shared world (i.e. a swarm), this
@@ -147,7 +145,6 @@ impl Client {
profile,
// default our id to 0, it'll be set later
entity,
- world: Arc::new(RwLock::new(PartialInstance::default())),
ecs,
@@ -268,7 +265,6 @@ impl Client {
read_conn,
write_conn,
),
- received_registries: ReceivedRegistries::default(),
local_player_events: LocalPlayerEvents(tx),
game_profile: GameProfileComponent(game_profile),
client_information: crate::ClientInformation::default(),
@@ -592,7 +588,6 @@ impl Client {
#[derive(Bundle)]
pub struct LocalPlayerBundle {
pub raw_connection: RawConnection,
- pub received_registries: ReceivedRegistries,
pub local_player_events: LocalPlayerEvents,
pub game_profile: GameProfileComponent,
pub client_information: ClientInformation,
@@ -604,7 +599,7 @@ pub struct LocalPlayerBundle {
/// use [`LocalEntity`].
#[derive(Bundle)]
pub struct JoinedClientBundle {
- pub instance_holder: InstanceHolder,
+ // note that InstanceHolder isn't here because it's set slightly before we fully join the world
pub physics_state: PhysicsState,
pub inventory: InventoryComponent,
pub tab_list: TabList,
@@ -679,11 +674,12 @@ async fn run_schedule_loop(
mut run_schedule_receiver: mpsc::UnboundedReceiver<()>,
) {
loop {
- // whenever we get an event from run_schedule_receiver, run the schedule
- run_schedule_receiver.recv().await;
// get rid of any queued events
while let Ok(()) = run_schedule_receiver.try_recv() {}
+ // whenever we get an event from run_schedule_receiver, run the schedule
+ run_schedule_receiver.recv().await;
+
let mut ecs = ecs.lock();
ecs.run_schedule(outer_schedule_label);
ecs.clear_trackers();
diff --git a/azalea-client/src/events.rs b/azalea-client/src/events.rs
index e5e30b6a..c85f142e 100644
--- a/azalea-client/src/events.rs
+++ b/azalea-client/src/events.rs
@@ -163,7 +163,7 @@ fn packet_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<Pac
for event in events.read() {
let local_player_events = query
.get(event.entity)
- .expect("Non-local entities shouldn't be able to receive add player events");
+ .expect("Non-local entities shouldn't be able to receive packet events");
local_player_events
.send(Event::Packet(event.packet.clone()))
.unwrap();
diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs
index bc5616e8..9d016bde 100644
--- a/azalea-client/src/lib.rs
+++ b/azalea-client/src/lib.rs
@@ -27,7 +27,6 @@ pub mod packet_handling;
pub mod ping;
mod player;
pub mod raw_connection;
-pub mod received_registries;
pub mod respawn;
pub mod task_pool;
@@ -42,4 +41,3 @@ pub use movement::{
PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection,
};
pub use player::PlayerInfo;
-pub use received_registries::ReceivedRegistries;
diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs
index db0a14f1..bebe841e 100644
--- a/azalea-client/src/local_player.rs
+++ b/azalea-client/src/local_player.rs
@@ -26,7 +26,7 @@ use crate::{
/// A component that keeps strong references to our [`PartialInstance`] and
/// [`Instance`] for local players.
-#[derive(Component)]
+#[derive(Component, Clone)]
pub struct InstanceHolder {
/// The partial instance is the world this client currently has loaded. It
/// has a limited render distance.
diff --git a/azalea-client/src/packet_handling/configuration.rs b/azalea-client/src/packet_handling/configuration.rs
index f35e25b2..ab0ce089 100644
--- a/azalea-client/src/packet_handling/configuration.rs
+++ b/azalea-client/src/packet_handling/configuration.rs
@@ -20,7 +20,6 @@ use crate::disconnect::DisconnectEvent;
use crate::local_player::Hunger;
use crate::packet_handling::game::KeepAliveEvent;
use crate::raw_connection::RawConnection;
-use crate::ReceivedRegistries;
#[derive(Event, Debug, Clone)]
pub struct PacketEvent {
@@ -78,19 +77,21 @@ pub fn process_packet_events(ecs: &mut World) {
for (player_entity, packet) in events_owned {
match packet {
ClientboundConfigurationPacket::RegistryData(p) => {
- let mut system_state: SystemState<Query<&mut ReceivedRegistries>> =
- SystemState::new(ecs);
- let mut query = system_state.get_mut(ecs);
- let mut received_registries = query.get_mut(player_entity).unwrap();
+ let mut instance = Instance::default();
- let new_received_registries = p.registry_holder.registries;
// override the old registries with the new ones
// but if a registry wasn't sent, keep the old one
- for (registry_name, registry) in new_received_registries {
- received_registries
- .registries
- .insert(registry_name, registry);
+ for (registry_name, registry) in p.registry_holder.map {
+ instance.registries.map.insert(registry_name, registry);
}
+
+ let instance_holder = crate::local_player::InstanceHolder::new(
+ player_entity,
+ // default to an empty world, it'll be set correctly later when we
+ // get the login packet
+ Arc::new(RwLock::new(instance)),
+ );
+ ecs.entity_mut(player_entity).insert(instance_holder);
}
ClientboundConfigurationPacket::CustomPayload(p) => {
@@ -113,13 +114,6 @@ pub fn process_packet_events(ecs: &mut World) {
let mut query = system_state.get_mut(ecs);
let mut raw_connection = query.get_mut(player_entity).unwrap();
- let instance_holder = crate::local_player::InstanceHolder::new(
- player_entity,
- // default to an empty world, it'll be set correctly later when we
- // get the login packet
- Arc::new(RwLock::new(Instance::default())),
- );
-
raw_connection
.write_packet(ServerboundFinishConfigurationPacket {}.get())
.expect(
@@ -131,7 +125,6 @@ pub fn process_packet_events(ecs: &mut World) {
ecs.entity_mut(player_entity)
.remove::<InConfigurationState>()
.insert(crate::JoinedClientBundle {
- instance_holder,
physics_state: crate::PhysicsState::default(),
inventory: crate::inventory::InventoryComponent::default(),
tab_list: crate::local_player::TabList::default(),
diff --git a/azalea-client/src/packet_handling/game.rs b/azalea-client/src/packet_handling/game.rs
index 71726142..9f7c5e69 100644
--- a/azalea-client/src/packet_handling/game.rs
+++ b/azalea-client/src/packet_handling/game.rs
@@ -45,7 +45,7 @@ use crate::{
},
movement::{KnockbackEvent, KnockbackType},
raw_connection::RawConnection,
- ClientInformation, PlayerInfo, ReceivedRegistries,
+ ClientInformation, PlayerInfo,
};
/// An event that's sent when we receive a packet.
@@ -174,16 +174,18 @@ pub fn send_packet_events(
}
pub fn process_packet_events(ecs: &mut World) {
- let mut events_owned = Vec::new();
- let mut system_state: SystemState<EventReader<PacketEvent>> = SystemState::new(ecs);
- let mut events = system_state.get_mut(ecs);
- for PacketEvent {
- entity: player_entity,
- packet,
- } in events.read()
+ let mut events_owned = Vec::<(Entity, Arc<ClientboundGamePacket>)>::new();
{
- // we do this so `ecs` isn't borrowed for the whole loop
- events_owned.push((*player_entity, packet.clone()));
+ let mut system_state = SystemState::<EventReader<PacketEvent>>::new(ecs);
+ let mut events = system_state.get_mut(ecs);
+ for PacketEvent {
+ entity: player_entity,
+ packet,
+ } in events.read()
+ {
+ // we do this so `ecs` isn't borrowed for the whole loop
+ events_owned.push((*player_entity, packet.clone()));
+ }
}
for (player_entity, packet) in events_owned {
let packet_clone = packet.clone();
@@ -198,7 +200,6 @@ pub fn process_packet_events(ecs: &mut World) {
Query<(
&GameProfileComponent,
&ClientInformation,
- &ReceivedRegistries,
Option<&mut InstanceName>,
Option<&mut LoadedBy>,
&mut EntityIdIndex,
@@ -220,7 +221,6 @@ pub fn process_packet_events(ecs: &mut World) {
let (
game_profile,
client_information,
- received_registries,
instance_name,
loaded_by,
mut entity_id_index,
@@ -238,7 +238,9 @@ pub fn process_packet_events(ecs: &mut World) {
.insert(InstanceName(new_instance_name.clone()));
}
- let Some(dimension_type) = received_registries.dimension_type() else {
+ let Some(dimension_type) =
+ instance_holder.instance.read().registries.dimension_type()
+ else {
error!("Server didn't send dimension type registry, can't log in");
continue;
};
@@ -276,6 +278,17 @@ pub fn process_packet_events(ecs: &mut World) {
// in a shared instance
Some(player_entity),
);
+ {
+ let new_registries = &mut weak_instance.write().registries;
+ // add the registries from this instance to the weak instance
+ for (registry_name, registry) in
+ &instance_holder.instance.read().registries.map
+ {
+ new_registries
+ .map
+ .insert(registry_name.clone(), registry.clone());
+ }
+ }
instance_holder.instance = weak_instance;
let player_bundle = PlayerBundle {
@@ -295,8 +308,6 @@ pub fn process_packet_events(ecs: &mut World) {
current: p.common.game_type,
previous: p.common.previous_game_type.into(),
},
- // this gets overwritten later by the SetHealth packet
- received_registries.clone(),
player_bundle,
));
@@ -583,10 +594,12 @@ pub fn process_packet_events(ecs: &mut World) {
let mut system_state: SystemState<Query<&mut InstanceHolder>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
- let local_player = query.get_mut(player_entity).unwrap();
- let mut partial_world = local_player.partial_instance.write();
+ let instance_holder = query.get_mut(player_entity).unwrap();
+ let mut partial_world = instance_holder.partial_instance.write();
- partial_world.chunks.view_center = ChunkPos::new(p.x, p.z);
+ partial_world
+ .chunks
+ .update_view_center(ChunkPos::new(p.x, p.z));
}
ClientboundGamePacket::ChunksBiomes(_) => {}
ClientboundGamePacket::LightUpdate(_p) => {
@@ -1167,7 +1180,18 @@ pub fn process_packet_events(ecs: &mut World) {
system_state.apply(ecs);
}
- ClientboundGamePacket::ForgetLevelChunk(_) => {}
+ ClientboundGamePacket::ForgetLevelChunk(p) => {
+ debug!("Got forget level chunk packet {p:?}");
+
+ let mut system_state: SystemState<Query<&mut InstanceHolder>> =
+ SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let local_player = query.get_mut(player_entity).unwrap();
+
+ let mut partial_instance = local_player.partial_instance.write();
+
+ partial_instance.chunks.limited_set(&p.pos, None);
+ }
ClientboundGamePacket::HorseScreenOpen(_) => {}
ClientboundGamePacket::MapItemData(_) => {}
ClientboundGamePacket::MerchantOffers(_) => {}
@@ -1255,20 +1279,21 @@ pub fn process_packet_events(ecs: &mut World) {
&mut InstanceHolder,
&GameProfileComponent,
&ClientInformation,
- &ReceivedRegistries,
)>,
EventWriter<InstanceLoadedEvent>,
ResMut<InstanceContainer>,
)> = SystemState::new(ecs);
let (mut commands, mut query, mut instance_loaded_events, mut instance_container) =
system_state.get_mut(ecs);
- let (mut instance_holder, game_profile, client_information, received_registries) =
+ let (mut instance_holder, game_profile, client_information) =
query.get_mut(player_entity).unwrap();
{
let new_instance_name = p.common.dimension.clone();
- let Some(dimension_type) = received_registries.dimension_type() else {
+ let Some(dimension_type) =
+ instance_holder.instance.read().registries.dimension_type()
+ else {
error!("Server didn't send dimension type registry, can't log in");
continue;
};
@@ -1298,6 +1323,7 @@ pub fn process_packet_events(ecs: &mut World) {
// set the partial_world to an empty world
// (when we add chunks or entities those will be in the
// instance_container)
+
*instance_holder.partial_instance.write() = PartialInstance::new(
azalea_world::chunk_storage::calculate_chunk_storage_range(
client_information.view_distance.into(),
diff --git a/azalea-client/src/received_registries.rs b/azalea-client/src/received_registries.rs
deleted file mode 100644
index f959a590..00000000
--- a/azalea-client/src/received_registries.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-use std::collections::HashMap;
-
-use azalea_core::resource_location::ResourceLocation;
-use azalea_nbt::Nbt;
-use azalea_protocol::packets::configuration::clientbound_registry_data_packet::registry::{
- DimensionTypeElement, RegistryType,
-};
-use bevy_ecs::prelude::*;
-use serde::de::DeserializeOwned;
-
-/// The registries that were sent to us during the configuration state.
-#[derive(Default, Component, Clone)]
-pub struct ReceivedRegistries {
- pub registries: HashMap<ResourceLocation, Nbt>,
-}
-
-impl ReceivedRegistries {
- fn get<T: DeserializeOwned>(&self, name: &ResourceLocation) -> Option<T> {
- let nbt = self.registries.get(name)?;
- serde_json::from_value(serde_json::to_value(nbt).ok()?).ok()
- }
-
- /// Get the dimension type registry, or `None` if it doesn't exist. You
- /// should do some type of error handling if this returns `None`.
- pub fn dimension_type(&self) -> Option<RegistryType<DimensionTypeElement>> {
- self.get(&ResourceLocation::new("minecraft:dimension_type"))
- }
-}
diff --git a/azalea-core/Cargo.toml b/azalea-core/Cargo.toml
index eb04ed01..a25485a2 100644
--- a/azalea-core/Cargo.toml
+++ b/azalea-core/Cargo.toml
@@ -18,6 +18,7 @@ nohash-hasher = "0.2.0"
num-traits = "0.2.17"
serde = { version = "^1.0", optional = true }
uuid = "^1.5.0"
+serde_json = "^1.0.108"
[features]
bevy_ecs = ["dep:bevy_ecs"]
diff --git a/azalea-core/src/lib.rs b/azalea-core/src/lib.rs
index 3b29f6a2..cb67da58 100755
--- a/azalea-core/src/lib.rs
+++ b/azalea-core/src/lib.rs
@@ -14,5 +14,6 @@ pub mod game_type;
pub mod math;
pub mod particle;
pub mod position;
+pub mod registry_holder;
pub mod resource_location;
pub mod tier;
diff --git a/azalea-core/src/registry_holder.rs b/azalea-core/src/registry_holder.rs
new file mode 100644
index 00000000..7f811e23
--- /dev/null
+++ b/azalea-core/src/registry_holder.rs
@@ -0,0 +1,412 @@
+//! The data sent to the client in the `ClientboundRegistryDataPacket`.
+//!
+//! This module contains the structures used to represent the registry
+//! sent to the client upon login. This contains a lot of information about
+//! the game, including the types of chat messages, dimensions, and
+//! biomes.
+
+use azalea_buf::{BufReadError, McBufReadable, McBufWritable};
+use azalea_nbt::Nbt;
+use serde::{
+ de::{self, DeserializeOwned},
+ Deserialize, Deserializer, Serialize, Serializer,
+};
+use std::{collections::HashMap, io::Cursor};
+
+use crate::resource_location::ResourceLocation;
+
+/// The base of the registry.
+///
+/// This is the registry that is sent to the client upon login.
+#[derive(Default, Debug, Clone, Serialize, Deserialize)]
+pub struct RegistryHolder {
+ pub map: HashMap<ResourceLocation, Nbt>,
+}
+
+impl RegistryHolder {
+ fn get<T: DeserializeOwned>(&self, name: &ResourceLocation) -> Option<T> {
+ let nbt = self.map.get(name)?;
+ serde_json::from_value(serde_json::to_value(nbt).ok()?).ok()
+ }
+
+ /// Get the dimension type registry, or `None` if it doesn't exist. You
+ /// should do some type of error handling if this returns `None`.
+ pub fn dimension_type(&self) -> Option<RegistryType<DimensionTypeElement>> {
+ self.get(&ResourceLocation::new("minecraft:dimension_type"))
+ }
+}
+
+impl TryFrom<Nbt> for RegistryHolder {
+ type Error = serde_json::Error;
+
+ fn try_from(value: Nbt) -> Result<Self, Self::Error> {
+ Ok(RegistryHolder {
+ map: serde_json::from_value(serde_json::to_value(value)?)?,
+ })
+ }
+}
+
+impl TryInto<Nbt> for RegistryHolder {
+ type Error = serde_json::Error;
+
+ fn try_into(self) -> Result<Nbt, Self::Error> {
+ serde_json::from_value(serde_json::to_value(self.map)?)
+ }
+}
+
+impl McBufReadable for RegistryHolder {
+ fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
+ RegistryHolder::try_from(Nbt::read_from(buf)?)
+ .map_err(|e| BufReadError::Deserialization { source: e })
+ }
+}
+
+impl McBufWritable for RegistryHolder {
+ fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
+ TryInto::<Nbt>::try_into(self.clone())?.write_into(buf)
+ }
+}
+
+/// A collection of values for a certain type of registry data.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct RegistryType<T> {
+ #[serde(rename = "type")]
+ pub kind: ResourceLocation,
+ pub value: Vec<TypeValue<T>>,
+}
+
+/// A value for a certain type of registry data.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct TypeValue<T> {
+ pub id: u32,
+ pub name: ResourceLocation,
+ pub element: T,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct TrimMaterialElement {
+ pub asset_name: String,
+ pub ingredient: ResourceLocation,
+ pub item_model_index: f32,
+ pub override_armor_materials: HashMap<String, String>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub description: Option<String>,
+}
+
+/// Data about a kind of chat message
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct ChatTypeElement {
+ pub chat: ChatTypeData,
+ pub narration: ChatTypeData,
+}
+
+/// Data about a chat message.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct ChatTypeData {
+ pub translation_key: String,
+ pub parameters: Vec<String>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub style: Option<ChatTypeStyle>,
+}
+
+/// The style of a chat message.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct ChatTypeStyle {
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub color: Option<String>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[serde(with = "Convert")]
+ pub bold: Option<bool>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[serde(with = "Convert")]
+ pub italic: Option<bool>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[serde(with = "Convert")]
+ pub underlined: Option<bool>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[serde(with = "Convert")]
+ pub strikethrough: Option<bool>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[serde(with = "Convert")]
+ pub obfuscated: Option<bool>,
+}
+
+/// Dimension attributes.
+#[cfg(feature = "strict_registry")]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct DimensionTypeElement {
+ pub ambient_light: f32,
+ #[serde(with = "Convert")]
+ pub bed_works: bool,
+ pub coordinate_scale: f32,
+ pub effects: ResourceLocation,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub fixed_time: Option<u32>,
+ #[serde(with = "Convert")]
+ pub has_ceiling: bool,
+ #[serde(with = "Convert")]
+ pub has_raids: bool,
+ #[serde(with = "Convert")]
+ pub has_skylight: bool,
+ pub height: u32,
+ pub infiniburn: ResourceLocation,
+ pub logical_height: u32,
+ pub min_y: i32,
+ pub monster_spawn_block_light_limit: u32,
+ pub monster_spawn_light_level: MonsterSpawnLightLevel,
+ #[serde(with = "Convert")]
+ pub natural: bool,
+ #[serde(with = "Convert")]
+ pub piglin_safe: bool,
+ #[serde(with = "Convert")]
+ pub respawn_anchor_works: bool,
+ #[serde(with = "Convert")]
+ pub ultrawarm: bool,
+}
+
+/// Dimension attributes.
+#[cfg(not(feature = "strict_registry"))]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DimensionTypeElement {
+ pub height: u32,
+ pub min_y: i32,
+ #[serde(flatten)]
+ pub _extra: HashMap<String, Nbt>,
+}
+
+/// The light level at which monsters can spawn.
+///
+/// This can be either a single minimum value, or a formula with a min and
+/// max.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub enum MonsterSpawnLightLevel {
+ /// A simple minimum value.
+ Simple(u32),
+ /// A complex value with a type, minimum, and maximum.
+ /// Vanilla minecraft only uses one type, "minecraft:uniform".
+ Complex {
+ #[serde(rename = "type")]
+ kind: ResourceLocation,
+ value: MonsterSpawnLightLevelValues,
+ },
+}
+
+/// The min and max light levels at which monsters can spawn.
+///
+/// Values are inclusive.
+#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct MonsterSpawnLightLevelValues {
+ #[serde(rename = "min_inclusive")]
+ pub min: u32,
+ #[serde(rename = "max_inclusive")]
+ pub max: u32,
+}
+
+/// Biome attributes.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct WorldTypeElement {
+ #[serde(with = "Convert")]
+ pub has_precipitation: bool,
+ pub temperature: f32,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub temperature_modifier: Option<String>,
+ pub downfall: f32,
+ pub effects: BiomeEffects,
+}
+
+/// The precipitation of a biome.
+#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub enum BiomePrecipitation {
+ #[serde(rename = "none")]
+ None,
+ #[serde(rename = "rain")]
+ Rain,
+ #[serde(rename = "snow")]
+ Snow,
+}
+
+/// The effects of a biome.
+///
+/// This includes the sky, fog, water, and grass color,
+/// as well as music and other sound effects.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct BiomeEffects {
+ pub sky_color: u32,
+ pub fog_color: u32,
+ pub water_color: u32,
+ pub water_fog_color: u32,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub foliage_color: Option<u32>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub grass_color: Option<u32>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub grass_color_modifier: Option<String>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub music: Option<BiomeMusic>,
+ pub mood_sound: BiomeMoodSound,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub additions_sound: Option<AdditionsSound>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub ambient_sound: Option<ResourceLocation>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub particle: Option<BiomeParticle>,
+}
+
+/// The music of the biome.
+///
+/// Some biomes have unique music that only play when inside them.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct BiomeMusic {
+ #[serde(with = "Convert")]
+ pub replace_current_music: bool,
+ pub max_delay: u32,
+ pub min_delay: u32,
+ pub sound: azalea_registry::SoundEvent,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct BiomeMoodSound {
+ pub tick_delay: u32,
+ pub block_search_extent: u32,
+ pub offset: f32,
+ pub sound: azalea_registry::SoundEvent,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct AdditionsSound {
+ pub tick_chance: f32,
+ pub sound: azalea_registry::SoundEvent,
+}
+
+/// Biome particles.
+///
+/// Some biomes have particles that spawn in the air.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct BiomeParticle {
+ pub probability: f32,
+ pub options: HashMap<String, String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct TrimPatternElement {
+ #[serde(flatten)]
+ pub pattern: HashMap<String, String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
+pub struct DamageTypeElement {
+ pub message_id: String,
+ pub scaling: String,
+ pub exhaustion: f32,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub effects: Option<String>,
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub death_message_type: Option<String>,
+}
+
+// Using a trait because you can't implement methods for
+// types you don't own, in this case Option<bool> and bool.
+trait Convert: Sized {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer;
+
+ fn deserialize<'de, D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>;
+}
+
+// Convert between bool and u8
+impl Convert for bool {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_u8(if *self { 1 } else { 0 })
+ }
+
+ fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ convert::<D>(u8::deserialize(deserializer)?)
+ }
+}
+
+// Convert between Option<bool> and u8
+impl Convert for Option<bool> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ if let Some(value) = self {
+ Convert::serialize(value, serializer)
+ } else {
+ serializer.serialize_none()
+ }
+ }
+
+ fn deserialize<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ if let Some(value) = Option::<u8>::deserialize(deserializer)? {
+ Ok(Some(convert::<D>(value)?))
+ } else {
+ Ok(None)
+ }
+ }
+}
+
+// Deserializing logic here to deduplicate code
+fn convert<'de, D>(value: u8) -> Result<bool, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ match value {
+ 0 => Ok(false),
+ 1 => Ok(true),
+ other => Err(de::Error::invalid_value(
+ de::Unexpected::Unsigned(other as u64),
+ &"zero or one",
+ )),
+ }
+}
diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs
index 6329184f..94b0cfde 100755
--- a/azalea-physics/src/collision/shape.rs
+++ b/azalea-physics/src/collision/shape.rs
@@ -52,7 +52,7 @@ pub fn box_shape_unchecked(
if x_bits < 0 || y_bits < 0 || z_bits < 0 {
return VoxelShape::Array(ArrayVoxelShape::new(
- BLOCK_SHAPE.shape(),
+ BLOCK_SHAPE.shape().to_owned(),
vec![min_x, max_x],
vec![min_y, max_y],
vec![min_z, max_z],
@@ -253,7 +253,14 @@ impl Shapes {
op_false_true,
);
- Self::matches_anywhere_with_mergers(x_merger, y_merger, z_merger, a.shape(), b.shape(), op)
+ Self::matches_anywhere_with_mergers(
+ x_merger,
+ y_merger,
+ z_merger,
+ a.shape().to_owned(),
+ b.shape().to_owned(),
+ op,
+ )
}
pub fn matches_anywhere_with_mergers(
@@ -347,7 +354,7 @@ impl VoxelShape {
}
}
- pub fn shape(&self) -> DiscreteVoxelShape {
+ pub fn shape(&self) -> &DiscreteVoxelShape {
match self {
VoxelShape::Array(s) => s.shape(),
VoxelShape::Cube(s) => s.shape(),
@@ -372,7 +379,7 @@ impl VoxelShape {
}
VoxelShape::Array(ArrayVoxelShape::new(
- self.shape(),
+ self.shape().to_owned(),
self.get_coords(Axis::X).iter().map(|c| c + x).collect(),
self.get_coords(Axis::Y).iter().map(|c| c + y).collect(),
self.get_coords(Axis::Z).iter().map(|c| c + z).collect(),
@@ -648,8 +655,8 @@ impl CubeVoxelShape {
}
impl ArrayVoxelShape {
- fn shape(&self) -> DiscreteVoxelShape {
- self.shape.clone()
+ fn shape(&self) -> &DiscreteVoxelShape {
+ &self.shape
}
fn get_coords(&self, axis: Axis) -> Vec<f64> {
@@ -658,8 +665,8 @@ impl ArrayVoxelShape {
}
impl CubeVoxelShape {
- fn shape(&self) -> DiscreteVoxelShape {
- self.shape.clone()
+ fn shape(&self) -> &DiscreteVoxelShape {
+ &self.shape
}
fn get_coords(&self, axis: Axis) -> Vec<f64> {
diff --git a/azalea-protocol/src/packets/configuration/clientbound_registry_data_packet.rs b/azalea-protocol/src/packets/configuration/clientbound_registry_data_packet.rs
index 08a1e880..0e782092 100644
--- a/azalea-protocol/src/packets/configuration/clientbound_registry_data_packet.rs
+++ b/azalea-protocol/src/packets/configuration/clientbound_registry_data_packet.rs
@@ -1,408 +1,8 @@
use azalea_buf::McBuf;
+use azalea_core::registry_holder::RegistryHolder;
use azalea_protocol_macros::ClientboundConfigurationPacket;
-use self::registry::RegistryHolder;
-
#[derive(Clone, Debug, McBuf, ClientboundConfigurationPacket)]
pub struct ClientboundRegistryDataPacket {
pub registry_holder: RegistryHolder,
}
-
-pub mod registry {
- //! [ClientboundRegistryDataPacket](super::ClientboundRegistryDataPacket)
- //! Registry Structures
- //!
- //! This module contains the structures used to represent the registry
- //! sent to the client upon login. This contains a lot of information about
- //! the game, including the types of chat messages, dimensions, and
- //! biomes.
-
- use azalea_buf::{BufReadError, McBufReadable, McBufWritable};
- use azalea_core::resource_location::ResourceLocation;
- use azalea_nbt::Nbt;
- use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
- use std::{collections::HashMap, io::Cursor};
-
- /// The base of the registry.
- ///
- /// This is the registry that is sent to the client upon login.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct RegistryHolder {
- pub registries: HashMap<ResourceLocation, Nbt>,
- }
-
- impl TryFrom<Nbt> for RegistryHolder {
- type Error = serde_json::Error;
-
- fn try_from(value: Nbt) -> Result<Self, Self::Error> {
- Ok(RegistryHolder {
- registries: serde_json::from_value(serde_json::to_value(value)?)?,
- })
- }
- }
-
- impl TryInto<Nbt> for RegistryHolder {
- type Error = serde_json::Error;
-
- fn try_into(self) -> Result<Nbt, Self::Error> {
- serde_json::from_value(serde_json::to_value(self.registries)?)
- }
- }
-
- impl McBufReadable for RegistryHolder {
- fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
- RegistryHolder::try_from(Nbt::read_from(buf)?)
- .map_err(|e| BufReadError::Deserialization { source: e })
- }
- }
-
- impl McBufWritable for RegistryHolder {
- fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
- TryInto::<Nbt>::try_into(self.clone())?.write_into(buf)
- }
- }
-
- /// A collection of values for a certain type of registry data.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct RegistryType<T> {
- #[serde(rename = "type")]
- pub kind: ResourceLocation,
- pub value: Vec<TypeValue<T>>,
- }
-
- /// A value for a certain type of registry data.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct TypeValue<T> {
- pub id: u32,
- pub name: ResourceLocation,
- pub element: T,
- }
-
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct TrimMaterialElement {
- pub asset_name: String,
- pub ingredient: ResourceLocation,
- pub item_model_index: f32,
- pub override_armor_materials: HashMap<String, String>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub description: Option<String>,
- }
-
- /// Data about a kind of chat message
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct ChatTypeElement {
- pub chat: ChatTypeData,
- pub narration: ChatTypeData,
- }
-
- /// Data about a chat message.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct ChatTypeData {
- pub translation_key: String,
- pub parameters: Vec<String>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub style: Option<ChatTypeStyle>,
- }
-
- /// The style of a chat message.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct ChatTypeStyle {
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub color: Option<String>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- #[serde(with = "Convert")]
- pub bold: Option<bool>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- #[serde(with = "Convert")]
- pub italic: Option<bool>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- #[serde(with = "Convert")]
- pub underlined: Option<bool>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- #[serde(with = "Convert")]
- pub strikethrough: Option<bool>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- #[serde(with = "Convert")]
- pub obfuscated: Option<bool>,
- }
-
- /// Dimension attributes.
- #[cfg(feature = "strict_registry")]
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[serde(deny_unknown_fields)]
- pub struct DimensionTypeElement {
- pub ambient_light: f32,
- #[serde(with = "Convert")]
- pub bed_works: bool,
- pub coordinate_scale: f32,
- pub effects: ResourceLocation,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub fixed_time: Option<u32>,
- #[serde(with = "Convert")]
- pub has_ceiling: bool,
- #[serde(with = "Convert")]
- pub has_raids: bool,
- #[serde(with = "Convert")]
- pub has_skylight: bool,
- pub height: u32,
- pub infiniburn: ResourceLocation,
- pub logical_height: u32,
- pub min_y: i32,
- pub monster_spawn_block_light_limit: u32,
- pub monster_spawn_light_level: MonsterSpawnLightLevel,
- #[serde(with = "Convert")]
- pub natural: bool,
- #[serde(with = "Convert")]
- pub piglin_safe: bool,
- #[serde(with = "Convert")]
- pub respawn_anchor_works: bool,
- #[serde(with = "Convert")]
- pub ultrawarm: bool,
- }
-
- /// Dimension attributes.
- #[cfg(not(feature = "strict_registry"))]
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct DimensionTypeElement {
- pub height: u32,
- pub min_y: i32,
- #[serde(flatten)]
- pub _extra: HashMap<String, Nbt>,
- }
-
- /// The light level at which monsters can spawn.
- ///
- /// This can be either a single minimum value, or a formula with a min and
- /// max.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[serde(untagged)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub enum MonsterSpawnLightLevel {
- /// A simple minimum value.
- Simple(u32),
- /// A complex value with a type, minimum, and maximum.
- /// Vanilla minecraft only uses one type, "minecraft:uniform".
- Complex {
- #[serde(rename = "type")]
- kind: ResourceLocation,
- value: MonsterSpawnLightLevelValues,
- },
- }
-
- /// The min and max light levels at which monsters can spawn.
- ///
- /// Values are inclusive.
- #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct MonsterSpawnLightLevelValues {
- #[serde(rename = "min_inclusive")]
- pub min: u32,
- #[serde(rename = "max_inclusive")]
- pub max: u32,
- }
-
- /// Biome attributes.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct WorldTypeElement {
- #[serde(with = "Convert")]
- pub has_precipitation: bool,
- pub temperature: f32,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub temperature_modifier: Option<String>,
- pub downfall: f32,
- pub effects: BiomeEffects,
- }
-
- /// The precipitation of a biome.
- #[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub enum BiomePrecipitation {
- #[serde(rename = "none")]
- None,
- #[serde(rename = "rain")]
- Rain,
- #[serde(rename = "snow")]
- Snow,
- }
-
- /// The effects of a biome.
- ///
- /// This includes the sky, fog, water, and grass color,
- /// as well as music and other sound effects.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct BiomeEffects {
- pub sky_color: u32,
- pub fog_color: u32,
- pub water_color: u32,
- pub water_fog_color: u32,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub foliage_color: Option<u32>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub grass_color: Option<u32>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub grass_color_modifier: Option<String>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub music: Option<BiomeMusic>,
- pub mood_sound: BiomeMoodSound,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub additions_sound: Option<AdditionsSound>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub ambient_sound: Option<ResourceLocation>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub particle: Option<BiomeParticle>,
- }
-
- /// The music of the biome.
- ///
- /// Some biomes have unique music that only play when inside them.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct BiomeMusic {
- #[serde(with = "Convert")]
- pub replace_current_music: bool,
- pub max_delay: u32,
- pub min_delay: u32,
- pub sound: azalea_registry::SoundEvent,
- }
-
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct BiomeMoodSound {
- pub tick_delay: u32,
- pub block_search_extent: u32,
- pub offset: f32,
- pub sound: azalea_registry::SoundEvent,
- }
-
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct AdditionsSound {
- pub tick_chance: f32,
- pub sound: azalea_registry::SoundEvent,
- }
-
- /// Biome particles.
- ///
- /// Some biomes have particles that spawn in the air.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct BiomeParticle {
- pub probability: f32,
- pub options: HashMap<String, String>,
- }
-
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct TrimPatternElement {
- #[serde(flatten)]
- pub pattern: HashMap<String, String>,
- }
-
- #[derive(Debug, Clone, Serialize, Deserialize)]
- #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
- pub struct DamageTypeElement {
- pub message_id: String,
- pub scaling: String,
- pub exhaustion: f32,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub effects: Option<String>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none")]
- pub death_message_type: Option<String>,
- }
-
- // Using a trait because you can't implement methods for
- // types you don't own, in this case Option<bool> and bool.
- trait Convert: Sized {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: Serializer;
-
- fn deserialize<'de, D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>;
- }
-
- // Convert between bool and u8
- impl Convert for bool {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: Serializer,
- {
- serializer.serialize_u8(if *self { 1 } else { 0 })
- }
-
- fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
- where
- D: Deserializer<'de>,
- {
- convert::<D>(u8::deserialize(deserializer)?)
- }
- }
-
- // Convert between Option<bool> and u8
- impl Convert for Option<bool> {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: Serializer,
- {
- if let Some(value) = self {
- Convert::serialize(value, serializer)
- } else {
- serializer.serialize_none()
- }
- }
-
- fn deserialize<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
- where
- D: Deserializer<'de>,
- {
- if let Some(value) = Option::<u8>::deserialize(deserializer)? {
- Ok(Some(convert::<D>(value)?))
- } else {
- Ok(None)
- }
- }
- }
-
- // Deserializing logic here to deduplicate code
- fn convert<'de, D>(value: u8) -> Result<bool, D::Error>
- where
- D: Deserializer<'de>,
- {
- match value {
- 0 => Ok(false),
- 1 => Ok(true),
- other => Err(de::Error::invalid_value(
- de::Unexpected::Unsigned(other as u64),
- &"zero or one",
- )),
- }
- }
-}
diff --git a/azalea-world/Cargo.toml b/azalea-world/Cargo.toml
index 2d83e1b2..4e0b4efa 100644
--- a/azalea-world/Cargo.toml
+++ b/azalea-world/Cargo.toml
@@ -26,6 +26,8 @@ once_cell = "1.18.0"
parking_lot = "^0.12.1"
thiserror = "1.0.50"
uuid = "1.5.0"
+serde_json = "1.0.108"
+serde = "1.0.192"
[dev-dependencies]
azalea-client = { path = "../azalea-client" }
diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs
index bbcbeb32..7301fdd1 100755
--- a/azalea-world/src/chunk_storage.rs
+++ b/azalea-world/src/chunk_storage.rs
@@ -23,9 +23,8 @@ const SECTION_HEIGHT: u32 = 16;
/// An efficient storage of chunks for a client that has a limited render
/// distance. This has support for using a shared [`ChunkStorage`].
pub struct PartialChunkStorage {
- /// The center of the view, i.e. the chunk the player is currently in. You
- /// can safely modify this.
- pub view_center: ChunkPos,
+ /// The center of the view, i.e. the chunk the player is currently in.
+ view_center: ChunkPos,
chunk_radius: u32,
view_range: u32,
// chunks is a list of size chunk_radius * chunk_radius
@@ -99,14 +98,47 @@ impl PartialChunkStorage {
}
}
- fn get_index(&self, chunk_pos: &ChunkPos) -> usize {
+ /// Update the chunk to center the view on. This should be called when the
+ /// client receives a `SetChunkCacheCenter` packet.
+ pub fn update_view_center(&mut self, view_center: ChunkPos) {
+ // this code block makes it force unload the chunks that are out of range after
+ // updating the view center. it's usually fine without it but the commented code
+ // is there in case you want to temporarily uncomment to test something
+
+ // ```
+ // for index in 0..self.chunks.len() {
+ // let chunk_pos = self.chunk_pos_from_index(index);
+ // if !in_range_for_view_center_and_radius(&chunk_pos, view_center, self.chunk_radius) {
+ // self.chunks[index] = None;
+ // }
+ // }
+ // ```
+
+ self.view_center = view_center;
+ }
+
+ /// Get the center of the view. This is usually the chunk that the player is
+ /// in.
+ pub fn view_center(&self) -> ChunkPos {
+ self.view_center
+ }
+
+ pub fn index_from_chunk_pos(&self, chunk_pos: &ChunkPos) -> usize {
(i32::rem_euclid(chunk_pos.x, self.view_range as i32) * (self.view_range as i32)
+ i32::rem_euclid(chunk_pos.z, self.view_range as i32)) as usize
}
+ pub fn chunk_pos_from_index(&self, index: usize) -> ChunkPos {
+ let x = index as i32 % self.view_range as i32;
+ let z = index as i32 / self.view_range as i32;
+ ChunkPos::new(
+ x + self.view_center.x - self.chunk_radius as i32,
+ z + self.view_center.z - self.chunk_radius as i32,
+ )
+ }
+
pub fn in_range(&self, chunk_pos: &ChunkPos) -> bool {
- (chunk_pos.x - self.view_center.x).unsigned_abs() <= self.chunk_radius
- && (chunk_pos.z - self.view_center.z).unsigned_abs() <= self.chunk_radius
+ in_range_for_view_center_and_radius(chunk_pos, self.view_center, self.chunk_radius)
}
pub fn set_block_state(
@@ -163,7 +195,7 @@ impl PartialChunkStorage {
return None;
}
- let index = self.get_index(pos);
+ let index = self.index_from_chunk_pos(pos);
self.chunks[index].as_ref()
}
/// Get a mutable reference to a [`Chunk`] within render distance, or
@@ -174,7 +206,7 @@ impl PartialChunkStorage {
return None;
}
- let index = self.get_index(pos);
+ let index = self.index_from_chunk_pos(pos);
Some(&mut self.chunks[index])
}
@@ -187,12 +219,13 @@ impl PartialChunkStorage {
pub fn set(&mut self, pos: &ChunkPos, chunk: Option<Chunk>, chunk_storage: &mut ChunkStorage) {
let new_chunk;
+ // add the chunk to the shared storage
if let Some(chunk) = chunk {
match chunk_storage.map.entry(*pos) {
Entry::Occupied(mut e) => {
if let Some(old_chunk) = e.get_mut().upgrade() {
*old_chunk.write() = chunk;
- new_chunk = Some(old_chunk.clone())
+ new_chunk = Some(old_chunk);
} else {
let chunk_lock = Arc::new(RwLock::new(chunk));
e.insert(Arc::downgrade(&chunk_lock));
@@ -211,33 +244,28 @@ impl PartialChunkStorage {
new_chunk = None;
}
- if let Some(chunk_mut) = self.limited_get_mut(pos) {
- *chunk_mut = new_chunk;
- }
+
+ self.limited_set(pos, new_chunk);
}
- /// Set a chunk in the shared storage and reference it from the limited
- /// storage. Use [`Self::set`] if you don't already have an
- /// `Arc<RwLock<Chunk>>` (it'll make it for you).
+ /// Set a chunk in our limited storage, useful if your chunk is already
+ /// referenced somewhere else and you want to make it also be referenced by
+ /// this storage.
+ ///
+ /// Use [`Self::set`] if you don't already have an `Arc<RwLock<Chunk>>`.
///
/// # Panics
/// If the chunk is not in the render distance.
- pub fn set_with_shared_reference(
- &mut self,
- pos: &ChunkPos,
- chunk: Option<Arc<RwLock<Chunk>>>,
- chunk_storage: &mut ChunkStorage,
- ) {
- if let Some(chunk) = &chunk {
- chunk_storage.map.insert(*pos, Arc::downgrade(chunk));
- } else {
- // don't remove it from the shared storage, since it'll be removed
- // automatically if this was the last reference
- }
+ pub fn limited_set(&mut self, pos: &ChunkPos, chunk: Option<Arc<RwLock<Chunk>>>) {
if let Some(chunk_mut) = self.limited_get_mut(pos) {
*chunk_mut = chunk;
}
}
+
+ /// Get an iterator over all the chunks in the storage.
+ pub fn chunks(&self) -> impl Iterator<Item = &Option<Arc<RwLock<Chunk>>>> {
+ self.chunks.iter()
+ }
}
impl ChunkStorage {
pub fn new(height: u32, min_y: i32) -> Self {
@@ -270,6 +298,15 @@ impl ChunkStorage {
}
}
+pub fn in_range_for_view_center_and_radius(
+ chunk_pos: &ChunkPos,
+ view_center: ChunkPos,
+ chunk_radius: u32,
+) -> bool {
+ (chunk_pos.x - view_center.x).unsigned_abs() <= chunk_radius
+ && (chunk_pos.z - view_center.z).unsigned_abs() <= chunk_radius
+}
+
impl Chunk {
pub fn read_with_dimension_height(
buf: &mut Cursor<&[u8]>,
diff --git a/azalea-world/src/container.rs b/azalea-world/src/container.rs
index 1e2dfef7..0b68ead6 100644
--- a/azalea-world/src/container.rs
+++ b/azalea-world/src/container.rs
@@ -1,4 +1,4 @@
-use azalea_core::resource_location::ResourceLocation;
+use azalea_core::{registry_holder::RegistryHolder, resource_location::ResourceLocation};
use bevy_ecs::{component::Component, system::Resource};
use derive_more::{Deref, DerefMut};
use nohash_hasher::IntMap;
@@ -70,6 +70,7 @@ impl InstanceContainer {
chunks: ChunkStorage::new(height, min_y),
entities_by_chunk: HashMap::new(),
entity_by_id: IntMap::default(),
+ registries: RegistryHolder::default(),
}));
self.instances.insert(name, Arc::downgrade(&world));
world
diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs
index a41b2530..7b6854f7 100644
--- a/azalea-world/src/world.rs
+++ b/azalea-world/src/world.rs
@@ -1,6 +1,7 @@
use crate::{iterators::ChunkIterator, palette::Palette, ChunkStorage, PartialChunkStorage};
use azalea_block::{BlockState, BlockStates, FluidState};
use azalea_core::position::{BlockPos, ChunkPos};
+use azalea_core::registry_holder::RegistryHolder;
use bevy_ecs::{component::Component, entity::Entity};
use derive_more::{Deref, DerefMut};
use nohash_hasher::IntMap;
@@ -87,6 +88,8 @@ pub struct Instance {
/// An index of Minecraft entity IDs to Azalea ECS entities. You should
/// avoid using this and instead use `azalea_entity::EntityIdIndex`
pub entity_by_id: IntMap<MinecraftEntityId, Entity>,
+
+ pub registries: RegistryHolder,
}
impl Instance {
@@ -237,6 +240,7 @@ impl From<ChunkStorage> for Instance {
chunks,
entities_by_chunk: HashMap::new(),
entity_by_id: IntMap::default(),
+ registries: RegistryHolder::default(),
}
}
}
diff --git a/azalea/examples/testbot.rs b/azalea/examples/testbot.rs
index c47aa061..2dd5b306 100644
--- a/azalea/examples/testbot.rs
+++ b/azalea/examples/testbot.rs
@@ -7,7 +7,7 @@ use azalea::inventory::ItemSlot;
use azalea::pathfinder::goals::BlockPosGoal;
use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection};
use azalea::{Account, Client, Event};
-use azalea_client::SprintDirection;
+use azalea_client::{InstanceHolder, SprintDirection};
use azalea_core::position::{ChunkBlockPos, ChunkPos, Vec3};
use azalea_protocol::packets::game::ClientboundGamePacket;
use azalea_world::heightmap::HeightmapKind;
@@ -47,7 +47,7 @@ async fn main() -> anyhow::Result<()> {
let mut accounts = Vec::new();
- for i in 0..3 {
+ for i in 0..100 {
accounts.push(Account::offline(&format!("bot{i}")));
}
@@ -56,7 +56,7 @@ async fn main() -> anyhow::Result<()> {
.add_accounts(accounts.clone())
.set_handler(handle)
.set_swarm_handler(swarm_handle)
- .join_delay(Duration::from_millis(100))
+ // .join_delay(Duration::from_millis(1000))
.start("localhost")
.await;
// let e = azalea::ClientBuilder::new()
@@ -80,7 +80,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.chat("Hello world");
}
Event::Chat(m) => {
- println!("client chat message: {}", m.content());
+ // println!("client chat message: {}", m.content());
if m.content() == bot.profile.name {
bot.chat("Bye");
tokio::time::sleep(Duration::from_millis(50)).await;
@@ -100,7 +100,6 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
let entity = bot.entity_by::<With<Player>, (&GameProfileComponent,)>(
|(profile,): &(&GameProfileComponent,)| profile.name == sender,
);
- println!("sender entity: {entity:?}");
match m.content().as_str() {
"whereami" => {
let Some(entity) = entity else {
@@ -308,6 +307,45 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.chat("no chunk found");
}
}
+ "debugchunks" => {
+ println!("shared:");
+
+ let partial_instance_lock = bot.component::<InstanceHolder>().partial_instance;
+ let local_chunk_storage = &partial_instance_lock.read().chunks;
+
+ let mut total_loaded_chunks_count = 0;
+ for (chunk_pos, chunk) in &bot.world().read().chunks.map {
+ if let Some(chunk) = chunk.upgrade() {
+ let in_range = local_chunk_storage.in_range(chunk_pos);
+ println!(
+ "{chunk_pos:?} has {} references{}",
+ std::sync::Arc::strong_count(&chunk) - 1,
+ if in_range { "" } else { " (out of range)" }
+ );
+ total_loaded_chunks_count += 1;
+ }
+ }
+
+ println!("local:");
+
+ let mut local_loaded_chunks_count = 0;
+ for (i, chunk) in local_chunk_storage.chunks().enumerate() {
+ if let Some(chunk) = chunk {
+ let chunk_pos = local_chunk_storage.chunk_pos_from_index(i);
+ println!(
+ "{chunk_pos:?} has {} references",
+ std::sync::Arc::strong_count(&chunk)
+ );
+ local_loaded_chunks_count += 1;
+ }
+ }
+
+ println!("total loaded chunks: {total_loaded_chunks_count}");
+ println!(
+ "local loaded chunks: {local_loaded_chunks_count}/{}",
+ local_chunk_storage.chunks().collect::<Vec<_>>().len()
+ );
+ }
_ => {}
}
}
diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs
index 05aabe68..0a263f39 100644
--- a/azalea/src/swarm/mod.rs
+++ b/azalea/src/swarm/mod.rs
@@ -394,9 +394,7 @@ where
let first_bot_state = first_bot.component::<S>();
let first_bot_entity = first_bot.entity;
- let mut tasks = Vec::new();
-
- tasks.push((handler)(first_bot, first_event, first_bot_state.clone()));
+ tokio::spawn((handler)(first_bot, first_event, first_bot_state.clone()));
// this makes it not have to keep locking the ecs
let mut states = HashMap::new();
@@ -405,10 +403,8 @@ where
let state = states
.entry(bot.entity)
.or_insert_with(|| bot.component::<S>().clone());
- tasks.push((handler)(bot, event, state.clone()));
+ tokio::spawn((handler)(bot, event, state.clone()));
}
-
- tokio::spawn(join_all(tasks));
}
}