From df9d776ff8e3945ce7d367e6cecb54957ee0fd7a Mon Sep 17 00:00:00 2001 From: Kumpelinus Date: Mon, 21 Jul 2025 22:28:41 +0200 Subject: [PATCH] Add TicksAlive component (#229) * Add TicksAlive component * Rename TicksAlive to TicksConnected * Move component to plugins/tick_counter.rs and add doc comment --- azalea-client/src/plugins/disconnect.rs | 4 +- azalea-client/src/plugins/mod.rs | 2 + azalea-client/src/plugins/packet/game/mod.rs | 18 ++----- azalea-client/src/plugins/tick_counter.rs | 37 +++++++++++++ azalea-client/tests/ticks_alive.rs | 57 ++++++++++++++++++++ 5 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 azalea-client/src/plugins/tick_counter.rs create mode 100644 azalea-client/tests/ticks_alive.rs diff --git a/azalea-client/src/plugins/disconnect.rs b/azalea-client/src/plugins/disconnect.rs index 80993476..ab39ba5e 100644 --- a/azalea-client/src/plugins/disconnect.rs +++ b/azalea-client/src/plugins/disconnect.rs @@ -11,7 +11,7 @@ use tracing::info; use super::login::IsAuthenticated; use crate::{ chat_signing, client::JoinedClientBundle, connection::RawConnection, loading::HasClientLoaded, - local_player::InstanceHolder, + local_player::InstanceHolder, tick_counter::TicksConnected, }; pub struct DisconnectPlugin; @@ -72,6 +72,8 @@ pub struct RemoveOnDisconnectBundle { pub is_authenticated: IsAuthenticated, // send ServerboundPlayerLoaded next time we join. pub has_client_loaded: HasClientLoaded, + // TickCounter is reset on reconnect + pub ticks_alive: TicksConnected, } /// A system that removes the several components from our clients when they get diff --git a/azalea-client/src/plugins/mod.rs b/azalea-client/src/plugins/mod.rs index 7c5cd3a3..2319f6fc 100644 --- a/azalea-client/src/plugins/mod.rs +++ b/azalea-client/src/plugins/mod.rs @@ -22,6 +22,7 @@ pub mod pong; pub mod respawn; pub mod task_pool; pub mod tick_broadcast; +pub mod tick_counter; pub mod tick_end; /// This plugin group will add all the default plugins necessary for Azalea to @@ -54,6 +55,7 @@ impl PluginGroup for DefaultPlugins { .add(loading::PlayerLoadedPlugin) .add(brand::BrandPlugin) .add(tick_broadcast::TickBroadcastPlugin) + .add(tick_counter::TickCounterPlugin) .add(pong::PongPlugin) .add(connection::ConnectionPlugin) .add(login::LoginPlugin) diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index fd6b712c..b7886208 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -22,22 +22,9 @@ pub use events::*; use tracing::{debug, error, trace, warn}; use crate::{ - ClientInformation, - block_update::QueuedServerBlockUpdates, - chat::{ChatPacket, ChatReceivedEvent}, - chunks, - connection::RawConnection, - declare_packet_handlers, - disconnect::DisconnectEvent, - interact::BlockStatePredictionHandler, - inventory::{ + block_update::QueuedServerBlockUpdates, chat::{ChatPacket, ChatReceivedEvent}, chunks, connection::RawConnection, declare_packet_handlers, disconnect::DisconnectEvent, interact::BlockStatePredictionHandler, inventory::{ ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent, - }, - loading::HasClientLoaded, - local_player::{Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList}, - movement::{KnockbackEvent, KnockbackType}, - packet::as_system, - player::{GameProfileComponent, PlayerInfo}, + }, loading::HasClientLoaded, local_player::{Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList}, movement::{KnockbackEvent, KnockbackType}, packet::as_system, player::{GameProfileComponent, PlayerInfo}, tick_counter::TicksConnected, ClientInformation }; pub fn process_packet(ecs: &mut World, player: Entity, packet: &ClientboundGamePacket) { @@ -299,6 +286,7 @@ impl GamePacketHandler<'_> { previous: p.common.previous_game_type.into(), }, entity_bundle, + TicksConnected(0), )); azalea_entity::indexing::add_entity_to_indexes( diff --git a/azalea-client/src/plugins/tick_counter.rs b/azalea-client/src/plugins/tick_counter.rs new file mode 100644 index 00000000..21515959 --- /dev/null +++ b/azalea-client/src/plugins/tick_counter.rs @@ -0,0 +1,37 @@ +use azalea_core::tick::GameTick; +use azalea_physics::PhysicsSet; +use azalea_world::InstanceName; + +use bevy_app::{App, Plugin}; +use bevy_ecs::prelude::*; + +use crate::{mining::MiningSet, movement::send_position, tick_broadcast::send_tick_broadcast}; + +/// Counts the number of game ticks elapsed on the **local client** since the +/// `login` packet was received. +#[derive(Component, Clone, Debug, Default)] +pub struct TicksConnected(pub u64); + +/// Inserts the counter-increment system into the `GameTick` schedule **before** +/// physics, mining and movement. +pub struct TickCounterPlugin; + +impl Plugin for TickCounterPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + GameTick, + increment_counter + .before(PhysicsSet) + .before(MiningSet) + .before(send_position) + .before(send_tick_broadcast), + ); + } +} + +/// Increment the [`GameTickCounter`] on every entity that lives in an instance. +fn increment_counter(mut query: Query<&mut TicksConnected, With>) { + for mut counter in &mut query { + counter.0 += 1; + } +} diff --git a/azalea-client/tests/ticks_alive.rs b/azalea-client/tests/ticks_alive.rs new file mode 100644 index 00000000..3be9a09c --- /dev/null +++ b/azalea-client/tests/ticks_alive.rs @@ -0,0 +1,57 @@ +use azalea_client::{test_utils::prelude::*, tick_counter::TicksConnected}; +use azalea_core::resource_location::ResourceLocation; +use azalea_protocol::packets::{config::{ClientboundFinishConfiguration, ClientboundRegistryData}, ConnectionProtocol}; +use azalea_registry::{DataRegistry, DimensionType}; +use simdnbt::owned::{NbtCompound, NbtTag}; + +#[test] +fn counter_increments_and_resets_on_disconnect() { + init_tracing(); + + let mut simulation = Simulation::new(ConnectionProtocol::Configuration); + + simulation.receive_packet(ClientboundRegistryData { + registry_id: ResourceLocation::new("minecraft:dimension_type"), + entries: vec![( + ResourceLocation::new("minecraft:overworld"), + Some(NbtCompound::from_values(vec![ + ("height".into(), NbtTag::Int(384)), + ("min_y".into(), NbtTag::Int(-64)), + ])), + )] + .into_iter() + .collect(), + }); + + simulation.receive_packet(ClientboundFinishConfiguration); + + simulation.tick(); + // we need a second tick to handle the state switch properly + simulation.tick(); + + assert!(!simulation.has_component::()); + + simulation.receive_packet(make_basic_login_packet( + DimensionType::new_raw(0), // overworld + ResourceLocation::new("minecraft:overworld"), + )); + simulation.tick(); + + assert!(simulation.has_component::()); + assert_eq!(simulation.component::().0, 1); + + // Tick three times; counter should read 2, 3, 4. + for expected in 2..=4 { + simulation.tick(); + let counter = simulation.component::(); + assert_eq!( + counter.0, expected, + "after {expected} tick(s) counter should be {expected}" + ); + } + + simulation.disconnect(); + simulation.tick(); + + assert!(!simulation.has_component::()); +}