1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

share registries in swarms and fix some bugs

This commit is contained in:
mat 2023-11-18 20:44:49 -06:00
parent 000abfa136
commit f0b58c7e74
21 changed files with 634 additions and 556 deletions

3
Cargo.lock generated
View file

@ -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",

View file

@ -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)?);
}

View file

@ -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}");
}

View file

@ -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();

View file

@ -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();

View file

@ -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;

View file

@ -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.

View file

@ -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(),

View file

@ -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(),

View file

@ -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"))
}
}

View file

@ -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"]

View file

@ -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;

View file

@ -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",
)),
}
}

View file

@ -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> {

View file

@ -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",
)),
}
}
}

View file

@ -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" }

View file

@ -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]>,

View file

@ -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

View file

@ -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(),
}
}
}

View file

@ -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()
);
}
_ => {}
}
}

View file

@ -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));
}
}