aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md5
-rw-r--r--azalea-client/src/client.rs4
-rw-r--r--azalea-client/src/plugins/block_update.rs49
-rw-r--r--azalea-client/src/plugins/chunks.rs4
-rw-r--r--azalea-client/src/plugins/disconnect.rs3
-rw-r--r--azalea-client/src/plugins/mod.rs2
-rw-r--r--azalea-client/src/plugins/movement.rs2
-rw-r--r--azalea-client/src/plugins/packet/game/mod.rs36
-rw-r--r--azalea-client/tests/packet_order.rs91
9 files changed, 157 insertions, 39 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0547f05..8e1c0931 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,17 +10,20 @@ is breaking anyways, semantic versioning is not followed.
### Added
-- Update to Minecraft 1.21.6.
- `HitResult` now contains the entity that's being looked at.
+- A `QueuedServerBlockUpdates` component that keeps track of block updates per `Update`.
### Changed
+- Update to Minecraft 1.21.6.
- Renamed `azalea_entity::EntityKind` to `EntityKindComponent` to disambiguate with `azalea_registry::EntityKind`.
- Moved functions and types related to hit results from `azalea::interact` to `azalea::interact::pick`.
- `Client::attack` now takes `Entity` instead of `MinecraftEntityId`.
### Fixed
+- Fix packet order for loading (`PlayerLoaded`/`MovePlayerPos`) and sprinting (`PlayerInput`/`PlayerCommand`).
+
## [0.13.0+mc1.21.5] - 2025-06-15
### Added
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index c9cc5259..9481ba2d 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -47,6 +47,7 @@ use uuid::Uuid;
use crate::{
Account, DefaultPlugins,
attack::{self},
+ block_update::QueuedServerBlockUpdates,
chunks::ChunkBatchInfo,
connection::RawConnection,
disconnect::DisconnectEvent,
@@ -586,7 +587,8 @@ pub struct JoinedClientBundle {
pub physics_state: PhysicsState,
pub inventory: Inventory,
pub tab_list: TabList,
- pub current_sequence_number: BlockStatePredictionHandler,
+ pub block_state_prediction_handler: BlockStatePredictionHandler,
+ pub queued_server_block_updates: QueuedServerBlockUpdates,
pub last_sent_direction: LastSentLookDirection,
pub abilities: PlayerAbilities,
pub permission_level: PermissionLevel,
diff --git a/azalea-client/src/plugins/block_update.rs b/azalea-client/src/plugins/block_update.rs
new file mode 100644
index 00000000..15e885b6
--- /dev/null
+++ b/azalea-client/src/plugins/block_update.rs
@@ -0,0 +1,49 @@
+use azalea_block::BlockState;
+use azalea_core::position::BlockPos;
+use bevy_app::{App, Plugin, Update};
+use bevy_ecs::prelude::*;
+
+use crate::{
+ chunks::handle_receive_chunk_event, interact::BlockStatePredictionHandler,
+ local_player::InstanceHolder,
+};
+
+pub struct BlockUpdatePlugin;
+impl Plugin for BlockUpdatePlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(
+ Update,
+ // has to be after ReceiveChunkEvent is handled so if we get chunk+blockupdate in one
+ // Update then the block update actually gets applied
+ handle_block_update_event.after(handle_receive_chunk_event),
+ );
+ }
+}
+
+/// A component that holds the list of block updates that need to be handled.
+///
+/// This is updated by `read_packets` (in `PreUpdate`) and handled/cleared by
+/// [`handle_block_update_event`] (`Update`).
+///
+/// This is a component instead of an ECS event for performance reasons.
+#[derive(Component, Debug, Clone, Default)]
+pub struct QueuedServerBlockUpdates {
+ pub list: Vec<(BlockPos, BlockState)>,
+}
+
+pub fn handle_block_update_event(
+ mut query: Query<(
+ &mut QueuedServerBlockUpdates,
+ &InstanceHolder,
+ &mut BlockStatePredictionHandler,
+ )>,
+) {
+ for (mut queued, instance_holder, mut prediction_handler) in query.iter_mut() {
+ let world = instance_holder.instance.read();
+ for (pos, block_state) in queued.list.drain(..) {
+ if !prediction_handler.update_known_server_state(pos, block_state) {
+ world.chunks.set_block_state(pos, block_state);
+ }
+ }
+ }
+}
diff --git a/azalea-client/src/plugins/chunks.rs b/azalea-client/src/plugins/chunks.rs
index 7a99c16c..a4e19f7f 100644
--- a/azalea-client/src/plugins/chunks.rs
+++ b/azalea-client/src/plugins/chunks.rs
@@ -28,7 +28,7 @@ impl Plugin for ChunksPlugin {
Update,
(
handle_chunk_batch_start_event,
- handle_receive_chunk_events,
+ handle_receive_chunk_event,
handle_chunk_batch_finished_event,
)
.chain()
@@ -65,7 +65,7 @@ pub struct ChunkBatchFinishedEvent {
pub batch_size: u32,
}
-pub fn handle_receive_chunk_events(
+pub fn handle_receive_chunk_event(
mut events: EventReader<ReceiveChunkEvent>,
mut query: Query<&InstanceHolder>,
) {
diff --git a/azalea-client/src/plugins/disconnect.rs b/azalea-client/src/plugins/disconnect.rs
index 8dddff09..80993476 100644
--- a/azalea-client/src/plugins/disconnect.rs
+++ b/azalea-client/src/plugins/disconnect.rs
@@ -56,6 +56,7 @@ pub struct DisconnectEvent {
#[derive(Bundle)]
pub struct RemoveOnDisconnectBundle {
pub joined_client: JoinedClientBundle,
+
pub entity: EntityBundle,
pub minecraft_entity_id: MinecraftEntityId,
pub instance_holder: InstanceHolder,
@@ -69,7 +70,7 @@ pub struct RemoveOnDisconnectBundle {
pub chat_signing_session: chat_signing::ChatSigningSession,
/// They're not authenticated anymore if they disconnected.
pub is_authenticated: IsAuthenticated,
- // send ServerboundPlayerLoaded next time we join
+ // send ServerboundPlayerLoaded next time we join.
pub has_client_loaded: HasClientLoaded,
}
diff --git a/azalea-client/src/plugins/mod.rs b/azalea-client/src/plugins/mod.rs
index 6f003b01..7c5cd3a3 100644
--- a/azalea-client/src/plugins/mod.rs
+++ b/azalea-client/src/plugins/mod.rs
@@ -2,6 +2,7 @@ use bevy_app::{PluginGroup, PluginGroupBuilder};
pub mod attack;
pub mod auto_reconnect;
+pub mod block_update;
pub mod brand;
pub mod chat;
pub mod chat_signing;
@@ -48,6 +49,7 @@ impl PluginGroup for DefaultPlugins {
.add(mining::MiningPlugin)
.add(attack::AttackPlugin)
.add(chunks::ChunksPlugin)
+ .add(block_update::BlockUpdatePlugin)
.add(tick_end::TickEndPlugin)
.add(loading::PlayerLoadedPlugin)
.add(brand::BrandPlugin)
diff --git a/azalea-client/src/plugins/movement.rs b/azalea-client/src/plugins/movement.rs
index 5d43261f..e97d9ec1 100644
--- a/azalea-client/src/plugins/movement.rs
+++ b/azalea-client/src/plugins/movement.rs
@@ -65,8 +65,8 @@ impl Plugin for MovementPlugin {
.in_set(PhysicsSet)
.before(ai_step)
.before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
- send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk),
send_player_input_packet,
+ send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk),
send_position.after(PhysicsSet),
)
.chain(),
diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs
index d9940937..fd6b712c 100644
--- a/azalea-client/src/plugins/packet/game/mod.rs
+++ b/azalea-client/src/plugins/packet/game/mod.rs
@@ -23,6 +23,7 @@ use tracing::{debug, error, trace, warn};
use crate::{
ClientInformation,
+ block_update::QueuedServerBlockUpdates,
chat::{ChatPacket, ChatReceivedEvent},
chunks,
connection::RawConnection,
@@ -1058,17 +1059,10 @@ impl GamePacketHandler<'_> {
pub fn block_update(&mut self, p: &ClientboundBlockUpdate) {
debug!("Got block update packet {p:?}");
- as_system::<Query<(&InstanceHolder, &mut BlockStatePredictionHandler)>>(
- self.ecs,
- |mut query| {
- let (local_player, mut prediction_handler) = query.get_mut(self.player).unwrap();
-
- let world = local_player.instance.read();
- if !prediction_handler.update_known_server_state(p.pos, p.block_state) {
- world.chunks.set_block_state(p.pos, p.block_state);
- }
- },
- );
+ as_system::<Query<&mut QueuedServerBlockUpdates>>(self.ecs, |mut query| {
+ let mut queued = query.get_mut(self.player).unwrap();
+ queued.list.push((p.pos, p.block_state));
+ });
}
pub fn animate(&mut self, p: &ClientboundAnimate) {
@@ -1078,19 +1072,13 @@ impl GamePacketHandler<'_> {
pub fn section_blocks_update(&mut self, p: &ClientboundSectionBlocksUpdate) {
debug!("Got section blocks update packet {p:?}");
- as_system::<Query<(&InstanceHolder, &mut BlockStatePredictionHandler)>>(
- self.ecs,
- |mut query| {
- let (local_player, mut prediction_handler) = query.get_mut(self.player).unwrap();
- let world = local_player.instance.read();
- for new_state in &p.states {
- let pos = p.section_pos + new_state.pos;
- if !prediction_handler.update_known_server_state(pos, new_state.state) {
- world.chunks.set_block_state(pos, new_state.state);
- }
- }
- },
- );
+ as_system::<Query<&mut QueuedServerBlockUpdates>>(self.ecs, |mut query| {
+ let mut queued = query.get_mut(self.player).unwrap();
+ for new_state in &p.states {
+ let pos = p.section_pos + new_state.pos;
+ queued.list.push((pos, new_state.state));
+ }
+ });
}
pub fn game_event(&mut self, p: &ClientboundGameEvent) {
diff --git a/azalea-client/tests/packet_order.rs b/azalea-client/tests/packet_order.rs
index 1d3b29a2..d4dd2558 100644
--- a/azalea-client/tests/packet_order.rs
+++ b/azalea-client/tests/packet_order.rs
@@ -1,26 +1,32 @@
use std::{collections::VecDeque, sync::Arc};
-use azalea_client::{packet::game::SendPacketEvent, test_utils::prelude::*};
+use azalea_client::{
+ SprintDirection, StartSprintEvent, packet::game::SendPacketEvent, test_utils::prelude::*,
+};
use azalea_core::{
- position::{ChunkPos, Vec3},
+ position::{BlockPos, ChunkPos, Vec3},
resource_location::ResourceLocation,
};
use azalea_entity::LookDirection;
use azalea_protocol::{
- common::movements::{PositionMoveRotation, RelativeMovements},
+ common::movements::{MoveFlags, PositionMoveRotation, RelativeMovements},
packets::{
ConnectionProtocol,
config::{ClientboundFinishConfiguration, ClientboundRegistryData},
- game::{ClientboundPlayerPosition, ServerboundAcceptTeleportation, ServerboundGamePacket},
+ game::{
+ ClientboundBlockUpdate, ClientboundPlayerPosition, ServerboundAcceptTeleportation,
+ ServerboundGamePacket, ServerboundMovePlayerPos, ServerboundMovePlayerPosRot,
+ ServerboundMovePlayerStatusOnly,
+ },
},
};
-use azalea_registry::{DataRegistry, DimensionType};
+use azalea_registry::{Block, DataRegistry, DimensionType};
use bevy_ecs::observer::Trigger;
use parking_lot::Mutex;
use simdnbt::owned::{NbtCompound, NbtTag};
#[test]
-fn test_set_health_before_login() {
+fn test_packet_order() {
init_tracing();
let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
@@ -53,16 +59,26 @@ fn test_set_health_before_login() {
// receive a chunk so the player is "loaded" now
simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(1, 1, 3),
+ block_state: Block::Stone.into(),
+ });
simulation.receive_packet(ClientboundPlayerPosition {
id: 1,
change: PositionMoveRotation {
- pos: Vec3::new(1., 2., 3.),
+ pos: Vec3::new(1.5, 2., 3.5),
delta: Vec3::ZERO,
look_direction: LookDirection::default(),
},
relative: RelativeMovements::all_absolute(),
});
simulation.tick();
+
+ assert_eq!(
+ simulation.get_block_state(BlockPos::new(1, 1, 3)),
+ Some(Block::Stone.into())
+ );
+
println!("sent_packets: {:?}", sent_packets.list.lock());
sent_packets.expect("AcceptTeleportation", |p| {
matches!(
@@ -71,7 +87,16 @@ fn test_set_health_before_login() {
)
});
sent_packets.expect("MovePlayerPosRot", |p| {
- matches!(p, ServerboundGamePacket::MovePlayerPosRot(_))
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPosRot(ServerboundMovePlayerPosRot {
+ flags: MoveFlags {
+ on_ground: false,
+ horizontal_collision: false
+ },
+ ..
+ })
+ )
});
// in vanilla these might be sent in a later tick (depending on how long it
@@ -81,11 +106,59 @@ fn test_set_health_before_login() {
matches!(p, ServerboundGamePacket::PlayerLoaded(_))
});
sent_packets.expect("MovePlayerPos", |p| {
- matches!(p, ServerboundGamePacket::MovePlayerPos(_))
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(ServerboundMovePlayerPos {
+ flags: MoveFlags {
+ on_ground: false,
+ horizontal_collision: false
+ },
+ ..
+ })
+ )
});
sent_packets.expect_tick_end();
sent_packets.expect_empty();
+
+ // it takes a tick for on_ground to be true
+ simulation.tick();
+ sent_packets.expect("MovePlayerStatusOnly", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerStatusOnly(ServerboundMovePlayerStatusOnly {
+ flags: MoveFlags {
+ on_ground: true,
+ horizontal_collision: false
+ }
+ })
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ // make sure nothing happens now
+ simulation.tick();
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ // now sprint for a tick
+ simulation.send_event(StartSprintEvent {
+ entity: simulation.entity,
+ direction: SprintDirection::Forward,
+ });
+ simulation.tick();
+ sent_packets.expect("PlayerInput", |p| {
+ matches!(p, ServerboundGamePacket::PlayerInput(_))
+ });
+ sent_packets.expect("PlayerCommand", |p| {
+ matches!(p, ServerboundGamePacket::PlayerCommand(_))
+ });
+ sent_packets.expect("MovePlayerPos", |p| {
+ matches!(p, ServerboundGamePacket::MovePlayerPos(_))
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
}
#[derive(Clone)]