diff options
| -rw-r--r-- | Cargo.lock | 3 | ||||
| -rwxr-xr-x | azalea-buf/src/read.rs | 11 | ||||
| -rw-r--r-- | azalea-client/src/chunks.rs | 34 | ||||
| -rw-r--r-- | azalea-client/src/client.rs | 14 | ||||
| -rw-r--r-- | azalea-client/src/events.rs | 2 | ||||
| -rw-r--r-- | azalea-client/src/lib.rs | 2 | ||||
| -rw-r--r-- | azalea-client/src/local_player.rs | 2 | ||||
| -rw-r--r-- | azalea-client/src/packet_handling/configuration.rs | 29 | ||||
| -rw-r--r-- | azalea-client/src/packet_handling/game.rs | 70 | ||||
| -rw-r--r-- | azalea-client/src/received_registries.rs | 28 | ||||
| -rw-r--r-- | azalea-core/Cargo.toml | 1 | ||||
| -rwxr-xr-x | azalea-core/src/lib.rs | 1 | ||||
| -rw-r--r-- | azalea-core/src/registry_holder.rs | 412 | ||||
| -rwxr-xr-x | azalea-physics/src/collision/shape.rs | 23 | ||||
| -rw-r--r-- | azalea-protocol/src/packets/configuration/clientbound_registry_data_packet.rs | 402 | ||||
| -rw-r--r-- | azalea-world/Cargo.toml | 2 | ||||
| -rwxr-xr-x | azalea-world/src/chunk_storage.rs | 91 | ||||
| -rw-r--r-- | azalea-world/src/container.rs | 3 | ||||
| -rw-r--r-- | azalea-world/src/world.rs | 4 | ||||
| -rw-r--r-- | azalea/examples/testbot.rs | 48 | ||||
| -rw-r--r-- | azalea/src/swarm/mod.rs | 8 |
21 files changed, 634 insertions, 556 deletions
@@ -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)); } } |
