aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/tests/simulation
diff options
context:
space:
mode:
Diffstat (limited to 'azalea-client/tests/simulation')
-rw-r--r--azalea-client/tests/simulation/change_dimension_to_nether_and_back.rs154
-rw-r--r--azalea-client/tests/simulation/client_disconnect.rs20
-rw-r--r--azalea-client/tests/simulation/close_open_container.rs67
-rw-r--r--azalea-client/tests/simulation/correct_movement.rs79
-rw-r--r--azalea-client/tests/simulation/correct_sneak_movement.rs111
-rw-r--r--azalea-client/tests/simulation/correct_sprint_sneak_movement.rs129
-rw-r--r--azalea-client/tests/simulation/despawn_entities_when_changing_dimension.rs81
-rw-r--r--azalea-client/tests/simulation/enchantments.rs124
-rw-r--r--azalea-client/tests/simulation/fast_login.rs42
-rw-r--r--azalea-client/tests/simulation/login_to_dimension_with_same_name.rs130
-rw-r--r--azalea-client/tests/simulation/mine_block_rollback.rs44
-rw-r--r--azalea-client/tests/simulation/mine_block_timing_hand.rs155
-rw-r--r--azalea-client/tests/simulation/mine_block_without_rollback.rs46
-rw-r--r--azalea-client/tests/simulation/mod.rs25
-rw-r--r--azalea-client/tests/simulation/move_and_despawn_entity.rs40
-rw-r--r--azalea-client/tests/simulation/move_despawned_entity.rs45
-rw-r--r--azalea-client/tests/simulation/packet_order.rs128
-rw-r--r--azalea-client/tests/simulation/packet_order_set_carried_item.rs110
-rw-r--r--azalea-client/tests/simulation/receive_spawn_entity_and_start_config_packet.rs36
-rw-r--r--azalea-client/tests/simulation/receive_start_config_packet.rs20
-rw-r--r--azalea-client/tests/simulation/reply_to_ping_with_pong.rs78
-rw-r--r--azalea-client/tests/simulation/set_health_before_login.rs50
-rw-r--r--azalea-client/tests/simulation/teleport_movement.rs158
-rw-r--r--azalea-client/tests/simulation/ticks_alive.rs32
24 files changed, 1904 insertions, 0 deletions
diff --git a/azalea-client/tests/simulation/change_dimension_to_nether_and_back.rs b/azalea-client/tests/simulation/change_dimension_to_nether_and_back.rs
new file mode 100644
index 00000000..2d4fb749
--- /dev/null
+++ b/azalea-client/tests/simulation/change_dimension_to_nether_and_back.rs
@@ -0,0 +1,154 @@
+use azalea_client::{InConfigState, InGameState, test_utils::prelude::*};
+use azalea_core::position::ChunkPos;
+use azalea_entity::LocalEntity;
+use azalea_protocol::packets::{
+ ConnectionProtocol, Packet,
+ config::{ClientboundFinishConfiguration, ClientboundRegistryData},
+};
+use azalea_registry::{DataRegistry, data::DimensionKind, identifier::Identifier};
+use azalea_world::InstanceName;
+use simdnbt::owned::{NbtCompound, NbtTag};
+
+#[test]
+fn test_change_dimension_to_nether_and_back() {
+ let _lock = init();
+
+ generic_test_change_dimension_to_nether_and_back(true);
+ generic_test_change_dimension_to_nether_and_back(false);
+}
+
+fn generic_test_change_dimension_to_nether_and_back(using_respawn: bool) {
+ let make_basic_login_or_respawn_packet = if using_respawn {
+ |dimension: DimensionKind, instance_name: Identifier| {
+ make_basic_respawn_packet(dimension, instance_name).into_variant()
+ }
+ } else {
+ |dimension: DimensionKind, instance_name: Identifier| {
+ make_basic_login_packet(dimension, instance_name).into_variant()
+ }
+ };
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
+ assert!(simulation.has_component::<InConfigState>());
+ assert!(!simulation.has_component::<InGameState>());
+
+ simulation.receive_packet(ClientboundRegistryData {
+ registry_id: Identifier::new("minecraft:dimension_type"),
+ entries: vec![
+ (
+ // this dimension should never be created. it just exists to make sure we're not
+ // hard-coding the dimension type id anywhere.
+ Identifier::new("azalea:fakedimension"),
+ Some(NbtCompound::from_values(vec![
+ ("height".into(), NbtTag::Int(16)),
+ ("min_y".into(), NbtTag::Int(0)),
+ ])),
+ ),
+ (
+ Identifier::new("minecraft:overworld"),
+ Some(NbtCompound::from_values(vec![
+ ("height".into(), NbtTag::Int(384)),
+ ("min_y".into(), NbtTag::Int(-64)),
+ ])),
+ ),
+ (
+ Identifier::new("minecraft:nether"),
+ Some(NbtCompound::from_values(vec![
+ ("height".into(), NbtTag::Int(256)),
+ ("min_y".into(), NbtTag::Int(0)),
+ ])),
+ ),
+ ]
+ .into_iter()
+ .collect(),
+ });
+ simulation.tick();
+ simulation.receive_packet(ClientboundFinishConfiguration);
+ simulation.tick();
+
+ assert!(!simulation.has_component::<InConfigState>());
+ assert!(simulation.has_component::<InGameState>());
+ assert!(simulation.has_component::<LocalEntity>());
+
+ //
+ // OVERWORLD
+ //
+
+ simulation.receive_packet(make_basic_login_packet(
+ DimensionKind::new_raw(1), // overworld
+ Identifier::new("azalea:a"),
+ ));
+ simulation.tick();
+
+ assert_eq!(
+ *simulation.component::<InstanceName>(),
+ Identifier::new("azalea:a"),
+ "InstanceName should be azalea:a after setting dimension to that"
+ );
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.tick();
+ // make sure the chunk exists
+ simulation
+ .chunk(ChunkPos::new(0, 0))
+ .expect("chunk should exist");
+
+ //
+ // NETHER
+ //
+
+ simulation.receive_packet(make_basic_login_or_respawn_packet(
+ DimensionKind::new_raw(2), // nether
+ Identifier::new("azalea:b"),
+ ));
+ simulation.tick();
+
+ assert!(
+ simulation.chunk(ChunkPos::new(0, 0)).is_none(),
+ "chunk should not exist immediately after changing dimensions"
+ );
+ assert_eq!(
+ *simulation.component::<InstanceName>(),
+ Identifier::new("azalea:b"),
+ "InstanceName should be azalea:b after changing dimensions to that"
+ );
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), 256 / 16));
+ simulation.tick();
+ // make sure the chunk exists
+ simulation
+ .chunk(ChunkPos::new(0, 0))
+ .expect("chunk should exist");
+ simulation.receive_packet(make_basic_login_or_respawn_packet(
+ DimensionKind::new_raw(2), // nether
+ Identifier::new("minecraft:nether"),
+ ));
+ simulation.tick();
+
+ //
+ // BACK TO OVERWORLD
+ //
+
+ simulation.receive_packet(make_basic_login_packet(
+ DimensionKind::new_raw(1), // overworld
+ Identifier::new("azalea:a"),
+ ));
+ simulation.tick();
+
+ assert_eq!(
+ *simulation.component::<InstanceName>(),
+ Identifier::new("azalea:a"),
+ "InstanceName should be azalea:a after setting dimension back to that"
+ );
+ assert!(
+ simulation.chunk(ChunkPos::new(0, 0)).is_none(),
+ "chunk should not exist immediately after switching back to overworld"
+ );
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.tick();
+ // make sure the chunk exists
+ simulation
+ .chunk(ChunkPos::new(0, 0))
+ .expect("chunk should exist");
+}
diff --git a/azalea-client/tests/simulation/client_disconnect.rs b/azalea-client/tests/simulation/client_disconnect.rs
new file mode 100644
index 00000000..0956fbfa
--- /dev/null
+++ b/azalea-client/tests/simulation/client_disconnect.rs
@@ -0,0 +1,20 @@
+use azalea_client::test_utils::prelude::*;
+use azalea_protocol::packets::ConnectionProtocol;
+use azalea_world::InstanceName;
+
+#[test]
+fn test_client_disconnect() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+
+ simulation.disconnect();
+ simulation.tick();
+
+ // make sure we're disconnected
+ let is_connected = simulation.has_component::<InstanceName>();
+ assert!(!is_connected);
+
+ // tick again to make sure nothing goes wrong
+ simulation.tick();
+}
diff --git a/azalea-client/tests/simulation/close_open_container.rs b/azalea-client/tests/simulation/close_open_container.rs
new file mode 100644
index 00000000..033451f1
--- /dev/null
+++ b/azalea-client/tests/simulation/close_open_container.rs
@@ -0,0 +1,67 @@
+use azalea_chat::FormattedText;
+use azalea_client::test_utils::prelude::*;
+use azalea_core::position::ChunkPos;
+use azalea_entity::inventory::Inventory;
+use azalea_protocol::packets::{
+ ConnectionProtocol,
+ game::{ClientboundContainerClose, ClientboundOpenScreen, ClientboundSetChunkCacheCenter},
+};
+use azalea_registry::builtin::MenuKind;
+
+#[test]
+fn test_close_open_container() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+ // receive a chunk so the player is "loaded" now
+ simulation.receive_packet(ClientboundSetChunkCacheCenter { x: 1, z: 23 });
+ simulation.receive_packet(make_basic_empty_chunk(
+ ChunkPos::new(1, 23),
+ (384 + 64) / 16,
+ ));
+ simulation.tick();
+
+ // ensure no container is open
+ simulation.with_component(|inventory: &Inventory| {
+ assert!(inventory.container_menu.is_none());
+ assert_eq!(inventory.id, 0);
+ });
+
+ // open a container
+ simulation.receive_packet(ClientboundOpenScreen {
+ container_id: 1,
+ menu_type: MenuKind::Generic9x3,
+ title: FormattedText::default(),
+ });
+ simulation.tick();
+
+ simulation.with_component(|inventory: &Inventory| {
+ assert!(inventory.container_menu.is_some());
+ assert_eq!(inventory.id, 1);
+ });
+
+ // close and open
+ simulation.receive_packet(ClientboundContainerClose { container_id: 1 });
+ simulation.receive_packet(ClientboundOpenScreen {
+ container_id: 2,
+ menu_type: MenuKind::Generic9x3,
+ title: FormattedText::default(),
+ });
+ simulation.tick();
+ simulation.with_component(|inventory: &Inventory| {
+ // ensure that the new container was opened
+ assert!(inventory.container_menu.is_some());
+ assert_eq!(inventory.id, 2);
+ });
+
+ // close with the wrong container id should still close
+ simulation.receive_packet(ClientboundContainerClose { container_id: 123 });
+ simulation.tick();
+ simulation.with_component(|inventory: &Inventory| {
+ assert!(inventory.container_menu.is_none());
+ assert_eq!(inventory.id, 0);
+ });
+}
diff --git a/azalea-client/tests/simulation/correct_movement.rs b/azalea-client/tests/simulation/correct_movement.rs
new file mode 100644
index 00000000..2a1a8f26
--- /dev/null
+++ b/azalea-client/tests/simulation/correct_movement.rs
@@ -0,0 +1,79 @@
+use azalea_client::{StartWalkEvent, WalkDirection, test_utils::prelude::*};
+use azalea_core::position::{BlockPos, ChunkPos, Vec3};
+use azalea_entity::LookDirection;
+use azalea_protocol::{
+ common::movements::{MoveFlags, PositionMoveRotation, RelativeMovements},
+ packets::{
+ ConnectionProtocol,
+ game::{
+ ClientboundBlockUpdate, ClientboundPlayerPosition, ClientboundSetChunkCacheCenter,
+ ServerboundGamePacket, ServerboundMovePlayerPos,
+ },
+ },
+};
+use azalea_registry::builtin::BlockKind;
+
+#[test]
+fn test_correct_movement() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ let sent_packets = SentPackets::new(&mut simulation);
+
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ // receive a chunk so the player is "loaded" now
+ simulation.receive_packet(ClientboundSetChunkCacheCenter { x: 1, z: 23 });
+ simulation.receive_packet(make_basic_empty_chunk(
+ ChunkPos::new(1, 23),
+ (384 + 64) / 16,
+ ));
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(31, 63, 370),
+ block_state: BlockKind::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 1,
+ change: PositionMoveRotation {
+ pos: Vec3::new(31.5, 64., 370.5),
+ delta: Vec3::ZERO,
+ look_direction: LookDirection::default(),
+ },
+ relative: RelativeMovements::all_absolute(),
+ });
+ simulation.tick();
+ simulation.tick();
+
+ // walk for a tick
+ simulation.write_message(StartWalkEvent {
+ entity: simulation.entity,
+ direction: WalkDirection::Forward,
+ });
+ sent_packets.clear();
+ simulation.tick();
+ sent_packets.expect("PlayerInput", |p| {
+ matches!(p, ServerboundGamePacket::PlayerInput(_))
+ });
+ sent_packets.expect("MovePlayerPos { pos.z: 370.59800000336764, ... }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(ServerboundMovePlayerPos {
+ pos: Vec3 {
+ x: 31.5,
+ y: 64.0,
+ z: 370.59800000336764
+ },
+ flags: MoveFlags {
+ on_ground: true,
+ horizontal_collision: false
+ }
+ })
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+}
diff --git a/azalea-client/tests/simulation/correct_sneak_movement.rs b/azalea-client/tests/simulation/correct_sneak_movement.rs
new file mode 100644
index 00000000..73abb26f
--- /dev/null
+++ b/azalea-client/tests/simulation/correct_sneak_movement.rs
@@ -0,0 +1,111 @@
+use azalea_client::{PhysicsState, StartWalkEvent, WalkDirection, test_utils::prelude::*};
+use azalea_core::position::{BlockPos, ChunkPos, Vec3};
+use azalea_entity::LookDirection;
+use azalea_protocol::{
+ common::movements::{PositionMoveRotation, RelativeMovements},
+ packets::{
+ ConnectionProtocol,
+ game::{
+ ClientboundBlockUpdate, ClientboundPlayerPosition, ServerboundGamePacket,
+ ServerboundPlayerInput,
+ },
+ },
+};
+use azalea_registry::builtin::BlockKind;
+
+#[test]
+fn test_correct_sneak_movement() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ let sent_packets = SentPackets::new(&mut simulation);
+
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(0, 119, 0),
+ block_state: BlockKind::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(0, 119, 1),
+ block_state: BlockKind::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 1,
+ change: PositionMoveRotation {
+ pos: Vec3::new(0.5, 120., 0.5),
+ delta: Vec3::ZERO,
+ look_direction: LookDirection::default(),
+ },
+ relative: RelativeMovements::all_absolute(),
+ });
+ simulation.tick();
+ simulation.tick();
+ simulation.tick();
+ sent_packets.clear();
+
+ simulation.with_component_mut::<PhysicsState>(|p| p.trying_to_crouch = true);
+ simulation.tick();
+ sent_packets.expect("PlayerInput", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::PlayerInput(p)
+ if *p == ServerboundPlayerInput { shift: true, ..Default::default() }
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ simulation.write_message(StartWalkEvent {
+ entity: simulation.entity,
+ direction: WalkDirection::Forward,
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("PlayerInput", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::PlayerInput(p)
+ if *p == ServerboundPlayerInput { forward: true, shift: true, ..Default::default() }
+ )
+ });
+ sent_packets.expect("MovePlayerPos { z: 0.5294000033944846 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 0.5294000033944846)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("MovePlayerPos { z: 0.5748524105068866 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 0.5748524105068866)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("MovePlayerPos: { z: 0.6290694310673044 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 0.6290694310673044)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+}
diff --git a/azalea-client/tests/simulation/correct_sprint_sneak_movement.rs b/azalea-client/tests/simulation/correct_sprint_sneak_movement.rs
new file mode 100644
index 00000000..a96c7024
--- /dev/null
+++ b/azalea-client/tests/simulation/correct_sprint_sneak_movement.rs
@@ -0,0 +1,129 @@
+use azalea_client::{PhysicsState, SprintDirection, StartSprintEvent, test_utils::prelude::*};
+use azalea_core::position::{BlockPos, ChunkPos, Vec3};
+use azalea_entity::LookDirection;
+use azalea_protocol::{
+ common::movements::{PositionMoveRotation, RelativeMovements},
+ packets::{
+ ConnectionProtocol,
+ game::{
+ ClientboundBlockUpdate, ClientboundPlayerPosition, ServerboundGamePacket,
+ ServerboundPlayerInput,
+ },
+ },
+};
+use azalea_registry::builtin::BlockKind;
+
+#[test]
+fn test_correct_sprint_sneak_movement() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ let sent_packets = SentPackets::new(&mut simulation);
+
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(0, 119, 0),
+ block_state: BlockKind::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(0, 119, 1),
+ block_state: BlockKind::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 1,
+ change: PositionMoveRotation {
+ pos: Vec3::new(0.5, 120., 0.5),
+ delta: Vec3::ZERO,
+ look_direction: LookDirection::default(),
+ },
+ relative: RelativeMovements::all_absolute(),
+ });
+ simulation.tick();
+ simulation.tick();
+ simulation.tick();
+ sent_packets.clear();
+
+ // start sprinting
+ simulation.write_message(StartSprintEvent {
+ entity: simulation.entity,
+ direction: SprintDirection::Forward,
+ });
+ simulation.tick();
+ sent_packets.expect("PlayerInput", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::PlayerInput(p)
+ if *p == ServerboundPlayerInput { forward: true, sprint: true, ..Default::default() }
+ )
+ });
+ sent_packets.expect("PlayerCommand", |p| {
+ matches!(p, ServerboundGamePacket::PlayerCommand(_))
+ });
+ sent_packets.expect("MovePlayerPos { z: 0.6274000124096872 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 0.6274000124096872)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("MovePlayerPos { z: 0.8243604396746886 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 0.8243604396746886)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+ simulation.with_component_mut::<PhysicsState>(|p| p.trying_to_crouch = true);
+
+ simulation.tick();
+ sent_packets.expect("PlayerInput", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::PlayerInput(p)
+ if *p == ServerboundPlayerInput { forward: true, sprint: true, shift: true, ..Default::default() }
+ )
+ });
+ sent_packets.expect("MovePlayerPos { z: 1.0593008578621674 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 1.0593008578621674)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("MovePlayerPos { z: 1.2257983479146455 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 1.2257983479146455)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("MovePlayerPos: { z: 1.3549259948648078 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 1.3549259948648078)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+}
diff --git a/azalea-client/tests/simulation/despawn_entities_when_changing_dimension.rs b/azalea-client/tests/simulation/despawn_entities_when_changing_dimension.rs
new file mode 100644
index 00000000..8619bb2d
--- /dev/null
+++ b/azalea-client/tests/simulation/despawn_entities_when_changing_dimension.rs
@@ -0,0 +1,81 @@
+use azalea_client::test_utils::prelude::*;
+use azalea_core::position::ChunkPos;
+use azalea_entity::metadata::Cow;
+use azalea_protocol::packets::{
+ ConnectionProtocol,
+ config::{ClientboundFinishConfiguration, ClientboundRegistryData},
+};
+use azalea_registry::{
+ DataRegistry, builtin::EntityKind, data::DimensionKind, identifier::Identifier,
+};
+use bevy_ecs::query::With;
+use simdnbt::owned::{NbtCompound, NbtTag};
+
+#[test]
+fn test_despawn_entities_when_changing_dimension() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
+ simulation.receive_packet(ClientboundRegistryData {
+ registry_id: Identifier::new("minecraft:dimension_type"),
+ entries: vec![
+ (
+ Identifier::new("minecraft:overworld"),
+ Some(NbtCompound::from_values(vec![
+ ("height".into(), NbtTag::Int(384)),
+ ("min_y".into(), NbtTag::Int(-64)),
+ ])),
+ ),
+ (
+ Identifier::new("minecraft:nether"),
+ Some(NbtCompound::from_values(vec![
+ ("height".into(), NbtTag::Int(256)),
+ ("min_y".into(), NbtTag::Int(0)),
+ ])),
+ ),
+ ]
+ .into_iter()
+ .collect(),
+ });
+ simulation.tick();
+ simulation.receive_packet(ClientboundFinishConfiguration);
+ simulation.tick();
+
+ //
+ // OVERWORLD
+ //
+
+ simulation.receive_packet(make_basic_login_packet(
+ DimensionKind::new_raw(0), // overworld
+ Identifier::new("azalea:a"),
+ ));
+ simulation.tick();
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.tick();
+ // spawn a cow
+ simulation.receive_packet(make_basic_add_entity(EntityKind::Cow, 123, (0.5, 64., 0.5)));
+ simulation.tick();
+ // make sure it's spawned
+ let mut cow_query = simulation.app.world_mut().query_filtered::<(), With<Cow>>();
+ let cow_iter = cow_query.iter(simulation.app.world());
+ assert_eq!(cow_iter.count(), 1, "cow should be spawned");
+
+ //
+ // NETHER
+ //
+
+ simulation.receive_packet(make_basic_respawn_packet(
+ DimensionKind::new_raw(1), // nether
+ Identifier::new("azalea:b"),
+ ));
+ simulation.tick();
+
+ // cow should be completely deleted from the ecs
+ let cow_iter = cow_query.iter(simulation.app.world());
+ assert_eq!(
+ cow_iter.count(),
+ 0,
+ "cow should be despawned after switching dimensions"
+ );
+}
diff --git a/azalea-client/tests/simulation/enchantments.rs b/azalea-client/tests/simulation/enchantments.rs
new file mode 100644
index 00000000..f9834230
--- /dev/null
+++ b/azalea-client/tests/simulation/enchantments.rs
@@ -0,0 +1,124 @@
+use azalea_client::test_utils::prelude::*;
+use azalea_entity::Attributes;
+use azalea_inventory::{ItemStack, components::Enchantments};
+use azalea_protocol::packets::{
+ ConnectionProtocol,
+ config::{ClientboundFinishConfiguration, ClientboundRegistryData},
+ game::ClientboundContainerSetSlot,
+};
+use azalea_registry::{Registry, builtin::ItemKind, data::Enchantment, identifier::Identifier};
+use simdnbt::owned::{NbtCompound, NbtTag};
+
+#[test]
+fn test_enchantments() {
+ let _lock = init();
+
+ let mut s = Simulation::new(ConnectionProtocol::Configuration);
+ s.receive_packet(ClientboundRegistryData {
+ registry_id: Identifier::new("minecraft:dimension_type"),
+ entries: vec![(
+ Identifier::new("minecraft:overworld"),
+ Some(NbtCompound::from_values(vec![
+ ("height".into(), NbtTag::Int(384)),
+ ("min_y".into(), NbtTag::Int(-64)),
+ ])),
+ )]
+ .into_iter()
+ .collect(),
+ });
+ // actual registry data copied from vanilla
+ s.receive_packet(ClientboundRegistryData {
+ registry_id: Identifier::new("minecraft:enchantment"),
+ entries: vec![(
+ Identifier::new("minecraft:efficiency"),
+ Some(NbtCompound::from([
+ (
+ "description",
+ [("translate", "enchantment.minecraft.efficiency".into())].into(),
+ ),
+ ("anvil_cost", 1.into()),
+ (
+ "max_cost",
+ [("base", 51.into()), ("per_level_above_first", 10.into())].into(),
+ ),
+ (
+ "min_cost",
+ [("base", 1.into()), ("per_level_above_first", 10.into())].into(),
+ ),
+ (
+ "effects",
+ [(
+ "minecraft:attributes",
+ [
+ ("operation", "add_value".into()),
+ ("attribute", "minecraft:mining_efficiency".into()),
+ (
+ "amount",
+ [
+ ("type", "minecraft:levels_squared".into()),
+ ("added", 1.0f32.into()),
+ ]
+ .into(),
+ ),
+ ("id", "minecraft:enchantment.efficiency".into()),
+ ]
+ .into(),
+ )]
+ .into(),
+ ),
+ ("max_level", 5.into()),
+ ("weight", 10.into()),
+ ("slots", ["mainhand"].into()),
+ ("supported_items", "#minecraft:enchantable/mining".into()),
+ ])),
+ )]
+ .into_iter()
+ .collect(),
+ });
+ s.tick();
+ s.receive_packet(ClientboundFinishConfiguration);
+ s.tick();
+ s.receive_packet(default_login_packet());
+ s.tick();
+
+ fn efficiency(simulation: &mut Simulation) -> f64 {
+ simulation.query_self::<&Attributes, _>(|c| c.mining_efficiency.calculate())
+ }
+
+ assert_eq!(efficiency(&mut s), 0.);
+
+ s.receive_packet(ClientboundContainerSetSlot {
+ container_id: 0,
+ state_id: 1,
+ slot: *azalea_inventory::Player::HOTBAR_SLOTS.start() as u16,
+ item_stack: ItemKind::DiamondPickaxe.into(),
+ });
+ s.tick();
+
+ // still 0 efficiency
+ assert_eq!(efficiency(&mut s), 0.);
+
+ s.receive_packet(ClientboundContainerSetSlot {
+ container_id: 0,
+ state_id: 2,
+ slot: *azalea_inventory::Player::HOTBAR_SLOTS.start() as u16,
+ item_stack: ItemStack::from(ItemKind::DiamondPickaxe).with_component(Enchantments {
+ levels: [(Enchantment::from_u32(0).unwrap(), 1)].into(),
+ }),
+ });
+ s.tick();
+
+ // level 1 gives us value 2
+ assert_eq!(efficiency(&mut s), 2.);
+
+ s.receive_packet(ClientboundContainerSetSlot {
+ container_id: 0,
+ state_id: 1,
+ slot: *azalea_inventory::Player::HOTBAR_SLOTS.start() as u16,
+ item_stack: ItemKind::DiamondPickaxe.into(),
+ });
+ s.tick();
+
+ // enchantment is cleared, so back to 0
+ assert_eq!(efficiency(&mut s), 0.);
+}
diff --git a/azalea-client/tests/simulation/fast_login.rs b/azalea-client/tests/simulation/fast_login.rs
new file mode 100644
index 00000000..270f4464
--- /dev/null
+++ b/azalea-client/tests/simulation/fast_login.rs
@@ -0,0 +1,42 @@
+use azalea_client::{InConfigState, test_utils::prelude::*};
+use azalea_entity::metadata::Health;
+use azalea_protocol::packets::{
+ ConnectionProtocol,
+ config::{ClientboundFinishConfiguration, ClientboundRegistryData},
+ game::ClientboundSetHealth,
+};
+use azalea_registry::identifier::Identifier;
+use simdnbt::owned::{NbtCompound, NbtTag};
+
+#[test]
+fn test_fast_login() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
+ assert!(simulation.has_component::<InConfigState>());
+
+ simulation.receive_packet(ClientboundRegistryData {
+ registry_id: Identifier::new("minecraft:dimension_type"),
+ entries: vec![(
+ Identifier::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);
+ // note that there's no simulation tick here
+ simulation.receive_packet(ClientboundSetHealth {
+ health: 15.,
+ food: 20,
+ saturation: 20.,
+ });
+ simulation.tick();
+ // we need a second tick to handle the state switch properly
+ simulation.tick();
+ assert_eq!(*simulation.component::<Health>(), 15.);
+}
diff --git a/azalea-client/tests/simulation/login_to_dimension_with_same_name.rs b/azalea-client/tests/simulation/login_to_dimension_with_same_name.rs
new file mode 100644
index 00000000..917c50bb
--- /dev/null
+++ b/azalea-client/tests/simulation/login_to_dimension_with_same_name.rs
@@ -0,0 +1,130 @@
+use azalea_client::{
+ InConfigState, InGameState, local_player::InstanceHolder, test_utils::prelude::*,
+};
+use azalea_core::position::ChunkPos;
+use azalea_entity::LocalEntity;
+use azalea_protocol::packets::{
+ ConnectionProtocol, Packet,
+ config::{ClientboundFinishConfiguration, ClientboundRegistryData},
+ game::ClientboundStartConfiguration,
+};
+use azalea_registry::{DataRegistry, data::DimensionKind, identifier::Identifier};
+use azalea_world::InstanceName;
+use simdnbt::owned::{NbtCompound, NbtTag};
+
+#[test]
+fn test_login_to_dimension_with_same_name() {
+ let _lock = init();
+
+ generic_test_login_to_dimension_with_same_name(true);
+ generic_test_login_to_dimension_with_same_name(false);
+}
+
+fn generic_test_login_to_dimension_with_same_name(using_respawn: bool) {
+ let make_basic_login_or_respawn_packet = if using_respawn {
+ |dimension: DimensionKind, instance_name: Identifier| {
+ make_basic_respawn_packet(dimension, instance_name).into_variant()
+ }
+ } else {
+ |dimension: DimensionKind, instance_name: Identifier| {
+ make_basic_login_packet(dimension, instance_name).into_variant()
+ }
+ };
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
+ assert!(simulation.has_component::<InConfigState>());
+ assert!(!simulation.has_component::<InGameState>());
+
+ simulation.receive_packet(ClientboundRegistryData {
+ registry_id: Identifier::new("minecraft:dimension_type"),
+ entries: vec![(
+ Identifier::new("minecraft:overworld"),
+ Some(NbtCompound::from_values(vec![
+ ("height".into(), NbtTag::Int(384)),
+ ("min_y".into(), NbtTag::Int(-64)),
+ ])),
+ )]
+ .into_iter()
+ .collect(),
+ });
+ simulation.tick();
+ simulation.receive_packet(ClientboundFinishConfiguration);
+ simulation.tick();
+
+ assert!(!simulation.has_component::<InConfigState>());
+ assert!(simulation.has_component::<InGameState>());
+ assert!(simulation.has_component::<LocalEntity>());
+
+ //
+ // OVERWORLD 1
+ //
+
+ simulation.receive_packet(make_basic_login_packet(
+ DimensionKind::new_raw(0), // overworld
+ Identifier::new("azalea:overworld"),
+ ));
+ simulation.tick();
+
+ assert_eq!(
+ *simulation.component::<InstanceName>(),
+ Identifier::new("azalea:overworld"),
+ "InstanceName should be azalea:overworld after setting dimension to that"
+ );
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.tick();
+ // make sure the chunk exists
+ simulation
+ .chunk(ChunkPos::new(0, 0))
+ .expect("chunk should exist");
+
+ //
+ // OVERWORLD 2
+ //
+
+ simulation.receive_packet(ClientboundStartConfiguration);
+ simulation.receive_packet(ClientboundRegistryData {
+ registry_id: Identifier::new("minecraft:dimension_type"),
+ entries: vec![(
+ Identifier::new("minecraft:overworld"),
+ Some(NbtCompound::from_values(vec![
+ ("height".into(), NbtTag::Int(256)),
+ ("min_y".into(), NbtTag::Int(0)),
+ ])),
+ )]
+ .into_iter()
+ .collect(),
+ });
+ simulation.receive_packet(ClientboundFinishConfiguration);
+ simulation.receive_packet(make_basic_login_or_respawn_packet(
+ DimensionKind::new_raw(0),
+ Identifier::new("azalea:overworld"),
+ ));
+ simulation.tick();
+
+ assert!(
+ simulation.chunk(ChunkPos::new(0, 0)).is_none(),
+ "chunk should not exist immediately after changing dimensions"
+ );
+ assert_eq!(
+ *simulation.component::<InstanceName>(),
+ Identifier::new("azalea:overworld"),
+ "InstanceName should still be azalea:overworld after changing dimensions to that"
+ );
+ assert_eq!(
+ simulation
+ .component::<InstanceHolder>()
+ .instance
+ .read()
+ .chunks
+ .height,
+ 256
+ );
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), 256 / 16));
+ simulation.tick();
+ // make sure the chunk exists
+ simulation
+ .chunk(ChunkPos::new(0, 0))
+ .expect("chunk should exist");
+}
diff --git a/azalea-client/tests/simulation/mine_block_rollback.rs b/azalea-client/tests/simulation/mine_block_rollback.rs
new file mode 100644
index 00000000..98440f76
--- /dev/null
+++ b/azalea-client/tests/simulation/mine_block_rollback.rs
@@ -0,0 +1,44 @@
+use azalea_client::{mining::StartMiningBlockEvent, test_utils::prelude::*};
+use azalea_core::position::{BlockPos, ChunkPos};
+use azalea_protocol::packets::{
+ ConnectionProtocol,
+ game::{ClientboundBlockChangedAck, ClientboundBlockUpdate},
+};
+use azalea_registry::builtin::BlockKind;
+
+#[test]
+fn test_mine_block_rollback() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ simulation.receive_packet(default_login_packet());
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.tick();
+
+ let pos = BlockPos::new(1, 2, 3);
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos,
+ // tnt is used for this test because it's insta-mineable so we don't have to waste ticks
+ // waiting
+ block_state: BlockKind::Tnt.into(),
+ });
+ simulation.tick();
+ assert_eq!(simulation.get_block_state(pos), Some(BlockKind::Tnt.into()));
+ println!("set serverside tnt");
+
+ simulation.write_message(StartMiningBlockEvent {
+ entity: simulation.entity,
+ position: pos,
+ force: true,
+ });
+ simulation.tick();
+ assert_eq!(simulation.get_block_state(pos), Some(BlockKind::Air.into()));
+ println!("set clientside air");
+
+ // server didn't send the new block, so the change should be rolled back
+ simulation.receive_packet(ClientboundBlockChangedAck { seq: 1 });
+ simulation.tick();
+ assert_eq!(simulation.get_block_state(pos), Some(BlockKind::Tnt.into()));
+ println!("reset serverside tnt");
+}
diff --git a/azalea-client/tests/simulation/mine_block_timing_hand.rs b/azalea-client/tests/simulation/mine_block_timing_hand.rs
new file mode 100644
index 00000000..53571089
--- /dev/null
+++ b/azalea-client/tests/simulation/mine_block_timing_hand.rs
@@ -0,0 +1,155 @@
+use azalea_client::{mining::StartMiningBlockEvent, test_utils::prelude::*};
+use azalea_core::{
+ direction::Direction,
+ position::{BlockPos, ChunkPos, Vec3},
+};
+use azalea_entity::LookDirection;
+use azalea_protocol::{
+ common::movements::{PositionMoveRotation, RelativeMovements},
+ packets::{
+ ConnectionProtocol, Packet,
+ game::{
+ ClientboundBlockUpdate, ClientboundPlayerPosition, ServerboundGamePacket,
+ ServerboundPlayerAction, ServerboundSwing, s_interact::InteractionHand,
+ s_player_action,
+ },
+ },
+};
+use azalea_registry::builtin::BlockKind;
+
+#[test]
+fn test_mine_block_timing_hand() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ let sent_packets = SentPackets::new(&mut simulation);
+ simulation.receive_packet(default_login_packet());
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.tick();
+
+ let pos = BlockPos::new(0, 2, 0);
+ let pos2 = BlockPos::new(0, 1, 0);
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos,
+ block_state: BlockKind::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: pos2,
+ block_state: BlockKind::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 1,
+ change: PositionMoveRotation {
+ pos: pos.up(1).center_bottom(),
+ delta: Vec3::ZERO,
+ look_direction: LookDirection::default(),
+ },
+ relative: RelativeMovements::all_absolute(),
+ });
+ simulation.tick();
+ assert_eq!(
+ simulation.get_block_state(pos),
+ Some(BlockKind::Stone.into())
+ );
+ println!("set serverside stone");
+ simulation.with_component_mut::<LookDirection>(|look| {
+ // look down
+ look.update_x_rot(90.);
+ });
+
+ simulation.tick();
+ simulation.tick();
+ simulation.tick();
+
+ simulation.write_message(StartMiningBlockEvent {
+ entity: simulation.entity,
+ position: pos,
+ force: false,
+ });
+ sent_packets.clear();
+ simulation.tick();
+ sent_packets.expect("PlayerAction", |p| {
+ p == &ServerboundPlayerAction {
+ action: s_player_action::Action::StartDestroyBlock,
+ pos,
+ direction: Direction::Up,
+ seq: 1,
+ }
+ .into_variant()
+ });
+ sent_packets.expect("Swing 1", |p| {
+ p == &ServerboundSwing {
+ hand: InteractionHand::MainHand,
+ }
+ .into_variant()
+ });
+ sent_packets.expect("Swing 2", |p| {
+ p == &ServerboundSwing {
+ hand: InteractionHand::MainHand,
+ }
+ .into_variant()
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ for i in 3..=151 {
+ simulation.tick();
+ sent_packets.expect(&format!("Swing {i}"), |p| {
+ p == &ServerboundSwing {
+ hand: InteractionHand::MainHand,
+ }
+ .into_variant()
+ });
+ sent_packets.maybe_expect(|p| matches!(p, ServerboundGamePacket::MovePlayerPos(_)));
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+ }
+
+ simulation.tick();
+ sent_packets.expect(
+ "ServerboundPlayerAction { action: StopDestroyBlock }",
+ |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::PlayerAction(p)
+ if p.action == s_player_action::Action::StopDestroyBlock
+ )
+ },
+ );
+ sent_packets.expect("Last swing", |p| {
+ p == &ServerboundSwing {
+ hand: InteractionHand::MainHand,
+ }
+ .into_variant()
+ });
+
+ for _ in 0..5 {
+ sent_packets.expect("MovePlayerPos", |p| {
+ matches!(p, ServerboundGamePacket::MovePlayerPos(_))
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+ simulation.tick();
+ }
+
+ // mine the block again to make sure that it takes the same number of ticks
+ simulation.write_message(StartMiningBlockEvent {
+ entity: simulation.entity,
+ position: pos2,
+ force: false,
+ });
+ for _ in 0..150 {
+ simulation.tick();
+ }
+ sent_packets.clear();
+ simulation.tick();
+
+ sent_packets.expect("PlayerAction { action: StopDestroyBlock }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::PlayerAction(p)
+ if p.action == s_player_action::Action::StopDestroyBlock
+ )
+ });
+}
diff --git a/azalea-client/tests/simulation/mine_block_without_rollback.rs b/azalea-client/tests/simulation/mine_block_without_rollback.rs
new file mode 100644
index 00000000..71f360c4
--- /dev/null
+++ b/azalea-client/tests/simulation/mine_block_without_rollback.rs
@@ -0,0 +1,46 @@
+use azalea_client::{mining::StartMiningBlockEvent, test_utils::prelude::*};
+use azalea_core::position::{BlockPos, ChunkPos};
+use azalea_protocol::packets::{
+ ConnectionProtocol,
+ game::{ClientboundBlockChangedAck, ClientboundBlockUpdate},
+};
+use azalea_registry::builtin::BlockKind;
+
+#[test]
+fn test_mine_block_without_rollback() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ simulation.receive_packet(default_login_packet());
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.tick();
+
+ let pos = BlockPos::new(1, 2, 3);
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos,
+ // tnt is used for this test because it's insta-mineable so we don't have to waste ticks
+ // waiting
+ block_state: BlockKind::Tnt.into(),
+ });
+ simulation.tick();
+ assert_eq!(simulation.get_block_state(pos), Some(BlockKind::Tnt.into()));
+
+ simulation.write_message(StartMiningBlockEvent {
+ entity: simulation.entity,
+ position: pos,
+ force: true,
+ });
+ simulation.tick();
+ assert_eq!(simulation.get_block_state(pos), Some(BlockKind::Air.into()));
+
+ // server acknowledged our change by sending a BlockUpdate + BlockChangedAck, so
+ // no rollback
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos,
+ block_state: BlockKind::Air.into(),
+ });
+ simulation.receive_packet(ClientboundBlockChangedAck { seq: 1 });
+ simulation.tick();
+ assert_eq!(simulation.get_block_state(pos), Some(BlockKind::Air.into()));
+}
diff --git a/azalea-client/tests/simulation/mod.rs b/azalea-client/tests/simulation/mod.rs
new file mode 100644
index 00000000..d090862b
--- /dev/null
+++ b/azalea-client/tests/simulation/mod.rs
@@ -0,0 +1,25 @@
+// This file is @generated by `azalea-client/build.rs`.
+
+mod change_dimension_to_nether_and_back;
+mod client_disconnect;
+mod close_open_container;
+mod correct_movement;
+mod correct_sneak_movement;
+mod correct_sprint_sneak_movement;
+mod despawn_entities_when_changing_dimension;
+mod enchantments;
+mod fast_login;
+mod login_to_dimension_with_same_name;
+mod mine_block_rollback;
+mod mine_block_timing_hand;
+mod mine_block_without_rollback;
+mod move_and_despawn_entity;
+mod move_despawned_entity;
+mod packet_order;
+mod packet_order_set_carried_item;
+mod receive_spawn_entity_and_start_config_packet;
+mod receive_start_config_packet;
+mod reply_to_ping_with_pong;
+mod set_health_before_login;
+mod teleport_movement;
+mod ticks_alive;
diff --git a/azalea-client/tests/simulation/move_and_despawn_entity.rs b/azalea-client/tests/simulation/move_and_despawn_entity.rs
new file mode 100644
index 00000000..6c334a47
--- /dev/null
+++ b/azalea-client/tests/simulation/move_and_despawn_entity.rs
@@ -0,0 +1,40 @@
+use azalea_client::test_utils::prelude::*;
+use azalea_core::position::{ChunkPos, Vec3};
+use azalea_protocol::{
+ common::movements::{PositionMoveRotation, RelativeMovements},
+ packets::{
+ ConnectionProtocol,
+ game::{ClientboundRemoveEntities, ClientboundTeleportEntity},
+ },
+};
+use azalea_registry::builtin::EntityKind;
+use azalea_world::MinecraftEntityId;
+
+#[test]
+fn test_move_and_despawn_entity() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ simulation.receive_packet(default_login_packet());
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.tick();
+
+ simulation.receive_packet(make_basic_add_entity(EntityKind::Cow, 123, (0.5, 64., 0.5)));
+ simulation.tick();
+
+ simulation.receive_packet(ClientboundTeleportEntity {
+ id: MinecraftEntityId(123),
+ change: PositionMoveRotation {
+ pos: Vec3::new(16., 0., 0.),
+ delta: Vec3::ZERO,
+ look_direction: Default::default(),
+ },
+ relative: RelativeMovements::all_relative(),
+ on_ground: true,
+ });
+ simulation.receive_packet(ClientboundRemoveEntities {
+ entity_ids: vec![MinecraftEntityId(123)],
+ });
+ simulation.tick();
+}
diff --git a/azalea-client/tests/simulation/move_despawned_entity.rs b/azalea-client/tests/simulation/move_despawned_entity.rs
new file mode 100644
index 00000000..7a171dae
--- /dev/null
+++ b/azalea-client/tests/simulation/move_despawned_entity.rs
@@ -0,0 +1,45 @@
+use azalea_client::test_utils::prelude::*;
+use azalea_core::position::ChunkPos;
+use azalea_entity::metadata::Cow;
+use azalea_protocol::packets::{ConnectionProtocol, game::ClientboundMoveEntityRot};
+use azalea_registry::builtin::EntityKind;
+use azalea_world::MinecraftEntityId;
+use bevy_ecs::query::With;
+use tracing::Level;
+
+#[test]
+fn test_move_despawned_entity() {
+ let _lock = init_with_level(Level::ERROR); // a warning is expected here
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ simulation.receive_packet(default_login_packet());
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.tick();
+ // spawn a cow
+ simulation.receive_packet(make_basic_add_entity(EntityKind::Cow, 123, (0.5, 64., 0.5)));
+ simulation.tick();
+
+ // make sure it's spawned
+ let mut cow_query = simulation.app.world_mut().query_filtered::<(), With<Cow>>();
+ let cow_iter = cow_query.iter(simulation.app.world());
+ assert_eq!(cow_iter.count(), 1, "cow should be spawned");
+
+ // despawn the cow by receiving a login packet
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+
+ // make sure it's despawned
+ let mut cow_query = simulation.app.world_mut().query_filtered::<(), With<Cow>>();
+ let cow_iter = cow_query.iter(simulation.app.world());
+ assert_eq!(cow_iter.count(), 0, "cow should be despawned");
+
+ // send a move_entity_rot
+ simulation.receive_packet(ClientboundMoveEntityRot {
+ entity_id: MinecraftEntityId(123),
+ y_rot: 0,
+ x_rot: 0,
+ on_ground: false,
+ });
+ simulation.tick();
+}
diff --git a/azalea-client/tests/simulation/packet_order.rs b/azalea-client/tests/simulation/packet_order.rs
new file mode 100644
index 00000000..ef99b938
--- /dev/null
+++ b/azalea-client/tests/simulation/packet_order.rs
@@ -0,0 +1,128 @@
+use azalea_client::{SprintDirection, StartSprintEvent, test_utils::prelude::*};
+use azalea_core::position::{BlockPos, ChunkPos, Vec3};
+use azalea_entity::LookDirection;
+use azalea_protocol::{
+ common::movements::{MoveFlags, PositionMoveRotation, RelativeMovements},
+ packets::{
+ ConnectionProtocol,
+ game::{
+ ClientboundBlockUpdate, ClientboundPlayerPosition, ServerboundAcceptTeleportation,
+ ServerboundGamePacket, ServerboundMovePlayerPosRot, ServerboundMovePlayerStatusOnly,
+ },
+ },
+};
+use azalea_registry::builtin::BlockKind;
+
+#[test]
+fn test_packet_order() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ let sent_packets = SentPackets::new(&mut simulation);
+
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ // 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: BlockKind::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 1,
+ change: PositionMoveRotation {
+ 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(BlockKind::Stone.into())
+ );
+ sent_packets.expect("AcceptTeleportation", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::AcceptTeleportation(ServerboundAcceptTeleportation { id: 1 })
+ )
+ });
+ sent_packets.expect("MovePlayerPosRot", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPosRot(p)
+ if p == &ServerboundMovePlayerPosRot {
+ flags: MoveFlags {
+ on_ground: false,
+ horizontal_collision: false
+ },
+ pos: Vec3::new(1.5, 2., 3.5),
+ look_direction: LookDirection::default(),
+ }
+ )
+ });
+
+ // in vanilla these might be sent in a later tick (depending on how long it
+ // takes to render the chunks)... see the comment in player_loaded_packet.
+ // this might be worth changing later for better anticheat compat?
+ sent_packets.expect("PlayerLoaded", |p| {
+ matches!(p, ServerboundGamePacket::PlayerLoaded(_))
+ });
+ sent_packets.expect("MovePlayerPos", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.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.write_message(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();
+}
diff --git a/azalea-client/tests/simulation/packet_order_set_carried_item.rs b/azalea-client/tests/simulation/packet_order_set_carried_item.rs
new file mode 100644
index 00000000..cae7c56a
--- /dev/null
+++ b/azalea-client/tests/simulation/packet_order_set_carried_item.rs
@@ -0,0 +1,110 @@
+use azalea_client::{
+ inventory::SetSelectedHotbarSlotEvent, mining::StartMiningBlockEvent, test_utils::prelude::*,
+};
+use azalea_core::{
+ direction::Direction,
+ position::{BlockPos, ChunkPos, Vec3},
+};
+use azalea_entity::LookDirection;
+use azalea_protocol::{
+ common::movements::{PositionMoveRotation, RelativeMovements},
+ packets::{
+ ConnectionProtocol, Packet,
+ game::{
+ ClientboundBlockUpdate, ClientboundPlayerPosition, ServerboundPlayerAction,
+ ServerboundSetCarriedItem, ServerboundSwing, s_interact::InteractionHand,
+ s_player_action,
+ },
+ },
+};
+use azalea_registry::builtin::BlockKind;
+
+#[test]
+fn test_packet_order_set_carried_item() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ let sent_packets = SentPackets::new(&mut simulation);
+ simulation.receive_packet(default_login_packet());
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.tick();
+
+ let pos = BlockPos::new(0, 2, 0);
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos,
+ block_state: BlockKind::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 1,
+ change: PositionMoveRotation {
+ pos: pos.up(1).center_bottom(),
+ delta: Vec3::ZERO,
+ look_direction: LookDirection::default(),
+ },
+ relative: RelativeMovements::all_absolute(),
+ });
+ simulation.tick();
+ assert_eq!(
+ simulation.get_block_state(pos),
+ Some(BlockKind::Stone.into())
+ );
+ simulation.with_component_mut::<LookDirection>(|look| {
+ // look down
+ look.update_x_rot(90.);
+ });
+
+ simulation.tick();
+ simulation.tick();
+ simulation.tick();
+
+ simulation.trigger(SetSelectedHotbarSlotEvent {
+ entity: simulation.entity,
+ slot: 1,
+ });
+ simulation.write_message(StartMiningBlockEvent {
+ entity: simulation.entity,
+ position: pos,
+ force: false,
+ });
+
+ sent_packets.clear();
+ simulation.tick();
+ sent_packets.expect("ServerboundPlayerAction", |p| {
+ p == &ServerboundPlayerAction {
+ action: s_player_action::Action::StartDestroyBlock,
+ pos,
+ direction: Direction::Up,
+ seq: 1,
+ }
+ .into_variant()
+ });
+ sent_packets.expect("Swing 1", |p| {
+ p == &ServerboundSwing {
+ hand: InteractionHand::MainHand,
+ }
+ .into_variant()
+ });
+ sent_packets.expect("SetCarriedItem", |p| {
+ p == &ServerboundSetCarriedItem { slot: 1 }.into_variant()
+ });
+ sent_packets.expect("Swing 2", |p| {
+ p == &ServerboundSwing {
+ hand: InteractionHand::MainHand,
+ }
+ .into_variant()
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+
+ sent_packets.expect("Swing", |p| {
+ p == &ServerboundSwing {
+ hand: InteractionHand::MainHand,
+ }
+ .into_variant()
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+}
diff --git a/azalea-client/tests/simulation/receive_spawn_entity_and_start_config_packet.rs b/azalea-client/tests/simulation/receive_spawn_entity_and_start_config_packet.rs
new file mode 100644
index 00000000..13dd38fc
--- /dev/null
+++ b/azalea-client/tests/simulation/receive_spawn_entity_and_start_config_packet.rs
@@ -0,0 +1,36 @@
+use azalea_client::{InConfigState, test_utils::prelude::*};
+use azalea_core::position::Vec3;
+use azalea_protocol::packets::{
+ ConnectionProtocol,
+ game::{ClientboundAddEntity, ClientboundStartConfiguration},
+};
+use azalea_registry::builtin::EntityKind;
+use azalea_world::InstanceName;
+use uuid::Uuid;
+
+#[test]
+fn test_receive_spawn_entity_and_start_config_packet() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+ assert!(simulation.has_component::<InstanceName>());
+ simulation.tick();
+
+ simulation.receive_packet(ClientboundAddEntity {
+ id: 123.into(),
+ uuid: Uuid::new_v4(),
+ entity_type: EntityKind::ArmorStand,
+ position: Vec3::ZERO,
+ x_rot: 0,
+ y_rot: 0,
+ y_head_rot: 0,
+ data: 0,
+ movement: Default::default(),
+ });
+ simulation.receive_packet(ClientboundStartConfiguration);
+
+ simulation.tick();
+ assert!(simulation.has_component::<InConfigState>());
+}
diff --git a/azalea-client/tests/simulation/receive_start_config_packet.rs b/azalea-client/tests/simulation/receive_start_config_packet.rs
new file mode 100644
index 00000000..f87d65da
--- /dev/null
+++ b/azalea-client/tests/simulation/receive_start_config_packet.rs
@@ -0,0 +1,20 @@
+use azalea_client::{InConfigState, test_utils::prelude::*};
+use azalea_protocol::packets::{ConnectionProtocol, game::ClientboundStartConfiguration};
+use azalea_world::InstanceName;
+
+#[test]
+fn test_receive_start_config_packet() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+ assert!(simulation.has_component::<InstanceName>());
+ simulation.tick();
+
+ simulation.receive_packet(ClientboundStartConfiguration);
+
+ simulation.tick();
+ assert!(simulation.has_component::<InConfigState>());
+}
diff --git a/azalea-client/tests/simulation/reply_to_ping_with_pong.rs b/azalea-client/tests/simulation/reply_to_ping_with_pong.rs
new file mode 100644
index 00000000..f77bf4bf
--- /dev/null
+++ b/azalea-client/tests/simulation/reply_to_ping_with_pong.rs
@@ -0,0 +1,78 @@
+use std::sync::Arc;
+
+use azalea_client::{
+ packet::{config::SendConfigPacketEvent, game::SendGamePacketEvent},
+ test_utils::prelude::*,
+};
+use azalea_protocol::packets::{
+ ConnectionProtocol,
+ config::{
+ self, ClientboundFinishConfiguration, ClientboundRegistryData, ServerboundConfigPacket,
+ },
+ game::{self, ServerboundGamePacket},
+};
+use azalea_registry::identifier::Identifier;
+use bevy_ecs::observer::On;
+use parking_lot::Mutex;
+use simdnbt::owned::{NbtCompound, NbtTag};
+
+#[test]
+fn reply_to_ping_with_pong() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
+
+ let reply_count = Arc::new(Mutex::new(0));
+ let reply_count_clone = reply_count.clone();
+ simulation
+ .app
+ .add_observer(move |send_config_packet: On<SendConfigPacketEvent>| {
+ if send_config_packet.sent_by == simulation.entity
+ && let ServerboundConfigPacket::Pong(packet) = &send_config_packet.packet
+ {
+ assert_eq!(packet.id, 321);
+ *reply_count_clone.lock() += 1;
+ }
+ });
+
+ simulation.receive_packet(config::ClientboundPing { id: 321 });
+ simulation.tick();
+ assert_eq!(*reply_count.lock(), 1);
+
+ // move into game state and test ClientboundPing there
+
+ simulation.receive_packet(ClientboundRegistryData {
+ registry_id: Identifier::new("minecraft:dimension_type"),
+ entries: vec![(
+ Identifier::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();
+
+ let reply_count = Arc::new(Mutex::new(0));
+ let reply_count_clone = reply_count.clone();
+ simulation
+ .app
+ .add_observer(move |send_game_packet: On<SendGamePacketEvent>| {
+ if send_game_packet.sent_by == simulation.entity
+ && let ServerboundGamePacket::Pong(packet) = &send_game_packet.packet
+ {
+ assert_eq!(packet.id, 123);
+ *reply_count_clone.lock() += 1;
+ }
+ });
+
+ simulation.tick();
+ simulation.receive_packet(game::ClientboundPing { id: 123 });
+ simulation.tick();
+
+ assert_eq!(*reply_count.lock(), 1);
+}
diff --git a/azalea-client/tests/simulation/set_health_before_login.rs b/azalea-client/tests/simulation/set_health_before_login.rs
new file mode 100644
index 00000000..7d183b80
--- /dev/null
+++ b/azalea-client/tests/simulation/set_health_before_login.rs
@@ -0,0 +1,50 @@
+use azalea_client::{InConfigState, test_utils::prelude::*};
+use azalea_entity::{LocalEntity, metadata::Health};
+use azalea_protocol::packets::{
+ ConnectionProtocol,
+ config::{ClientboundFinishConfiguration, ClientboundRegistryData},
+ game::ClientboundSetHealth,
+};
+use azalea_registry::identifier::Identifier;
+use simdnbt::owned::{NbtCompound, NbtTag};
+
+#[test]
+fn test_set_health_before_login() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
+ assert!(simulation.has_component::<InConfigState>());
+
+ simulation.receive_packet(ClientboundRegistryData {
+ registry_id: Identifier::new("minecraft:dimension_type"),
+ entries: vec![(
+ Identifier::new("minecraft:overworld"),
+ Some(NbtCompound::from_values(vec![
+ ("height".into(), NbtTag::Int(384)),
+ ("min_y".into(), NbtTag::Int(-64)),
+ ])),
+ )]
+ .into_iter()
+ .collect(),
+ });
+ simulation.tick();
+ simulation.receive_packet(ClientboundFinishConfiguration);
+ simulation.tick();
+
+ assert!(!simulation.has_component::<InConfigState>());
+ assert!(simulation.has_component::<LocalEntity>());
+
+ simulation.receive_packet(ClientboundSetHealth {
+ health: 15.,
+ food: 20,
+ saturation: 20.,
+ });
+ simulation.tick();
+ assert_eq!(*simulation.component::<Health>(), 15.);
+
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+
+ // health should stay the same
+ assert_eq!(*simulation.component::<Health>(), 15.);
+}
diff --git a/azalea-client/tests/simulation/teleport_movement.rs b/azalea-client/tests/simulation/teleport_movement.rs
new file mode 100644
index 00000000..06a8f0b6
--- /dev/null
+++ b/azalea-client/tests/simulation/teleport_movement.rs
@@ -0,0 +1,158 @@
+use azalea_client::test_utils::prelude::*;
+use azalea_core::{
+ delta::{LpVec3, PositionDelta8},
+ position::{BlockPos, ChunkPos, Vec3},
+};
+use azalea_entity::LookDirection;
+use azalea_protocol::{
+ common::movements::{MoveFlags, PositionMoveRotation, RelativeMovements},
+ packets::{
+ ConnectionProtocol,
+ game::{
+ ClientboundBlockUpdate, ClientboundForgetLevelChunk, ClientboundPing,
+ ClientboundPlayerPosition, ClientboundSetChunkCacheCenter, ClientboundSetEntityMotion,
+ ServerboundGamePacket, ServerboundMovePlayerPos, ServerboundMovePlayerPosRot,
+ },
+ },
+};
+use azalea_registry::builtin::BlockKind;
+use azalea_world::MinecraftEntityId;
+
+#[test]
+fn test_teleport_movement() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ let sent_packets = SentPackets::new(&mut simulation);
+
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ // receive a chunk so the player is "loaded" now
+ simulation.receive_packet(ClientboundSetChunkCacheCenter { x: 1, z: 23 });
+ simulation.receive_packet(make_basic_empty_chunk(
+ ChunkPos::new(1, 23),
+ (384 + 64) / 16,
+ ));
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(31, 63, 370),
+ block_state: BlockKind::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 1,
+ change: PositionMoveRotation {
+ pos: Vec3::new(31.5, 64., 370.5),
+ delta: Vec3::ZERO,
+ look_direction: LookDirection::default(),
+ },
+ relative: RelativeMovements::all_absolute(),
+ });
+ simulation.tick();
+ simulation.tick();
+ sent_packets.clear();
+
+ // now teleport to a far-away location
+ tracing::info!("meow!");
+ simulation.receive_packet(ClientboundPing { id: 1 });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 2,
+ change: PositionMoveRotation {
+ pos: Vec3::new(10000.5, 70.0, 0.5),
+ delta: Vec3::ZERO,
+ look_direction: LookDirection::default(),
+ },
+ relative: RelativeMovements::all_absolute(),
+ });
+ simulation.receive_packet(ClientboundPing { id: 2 });
+ simulation.tick();
+ sent_packets.expect_pong(1);
+ sent_packets.expect("AcceptTeleportation", |p| {
+ matches!(p, ServerboundGamePacket::AcceptTeleportation(_))
+ });
+ sent_packets.expect("MovePlayerPosRot", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPosRot(p)
+ if p == &ServerboundMovePlayerPosRot {
+ pos: Vec3::new(10000.5, 70.0, 0.5),
+ flags: MoveFlags::default(),
+ look_direction: LookDirection::default(),
+ }
+ )
+ });
+ sent_packets.expect_pong(2);
+ sent_packets.expect("MovePlayerPos", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p == &ServerboundMovePlayerPos {
+ pos: Vec3::new(10000.5, 70.0, 0.5),
+ flags: MoveFlags::default()
+ }
+ )
+ });
+
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ //
+
+ simulation.receive_packet(ClientboundForgetLevelChunk {
+ pos: ChunkPos { x: 1, z: 23 },
+ });
+ simulation.receive_packet(ClientboundSetChunkCacheCenter { x: 625, z: 0 });
+ simulation.receive_packet(ClientboundPing { id: 3 });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 3,
+ change: PositionMoveRotation {
+ pos: Vec3::new(10000.5, 70.0000001, 0.5),
+ delta: Vec3::ZERO,
+ look_direction: LookDirection::default(),
+ },
+ relative: RelativeMovements::all_absolute(),
+ });
+ simulation.receive_packet(ClientboundPing { id: 4 });
+ simulation.receive_packet(ClientboundSetEntityMotion {
+ id: MinecraftEntityId(0),
+ delta: LpVec3::from(Vec3::from(PositionDelta8 {
+ xa: 0,
+ ya: -627,
+ za: 0,
+ })),
+ });
+ simulation.receive_packet(ClientboundPing { id: 5 });
+ simulation.tick();
+
+ sent_packets.expect_pong(3);
+ sent_packets.expect("AcceptTeleportation", |p| {
+ matches!(p, ServerboundGamePacket::AcceptTeleportation(_))
+ });
+ sent_packets.expect("MovePlayerPosRot", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPosRot(p)
+ if p == &ServerboundMovePlayerPosRot {
+ pos: Vec3::new(10000.5, 70.0000001, 0.5),
+ flags: MoveFlags::default(),
+ look_direction: LookDirection::default(),
+ })
+ });
+ sent_packets.expect_pong(4);
+ sent_packets.expect_pong(5);
+ sent_packets.expect("MovePlayerPos", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p == &ServerboundMovePlayerPos {
+ pos: Vec3::new(10000.5, 69.84691458452664, 0.5),
+ flags: MoveFlags::default()
+ }
+ )
+ });
+
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+}
diff --git a/azalea-client/tests/simulation/ticks_alive.rs b/azalea-client/tests/simulation/ticks_alive.rs
new file mode 100644
index 00000000..655504db
--- /dev/null
+++ b/azalea-client/tests/simulation/ticks_alive.rs
@@ -0,0 +1,32 @@
+use azalea_client::{test_utils::prelude::*, tick_counter::TicksConnected};
+use azalea_protocol::packets::ConnectionProtocol;
+
+#[test]
+fn counter_increments_and_resets_on_disconnect() {
+ let _lock = init();
+
+ let mut simulation = Simulation::new(ConnectionProtocol::Game);
+ simulation.tick();
+
+ assert!(!simulation.has_component::<TicksConnected>());
+ simulation.receive_packet(default_login_packet());
+ simulation.tick();
+
+ assert!(simulation.has_component::<TicksConnected>());
+ assert_eq!(simulation.component::<TicksConnected>().0, 1);
+
+ // Tick three times; counter should read 2, 3, 4.
+ for expected in 2..=4 {
+ simulation.tick();
+ let counter = simulation.component::<TicksConnected>();
+ assert_eq!(
+ counter.0, expected,
+ "after {expected} tick(s) counter should be {expected}"
+ );
+ }
+
+ simulation.disconnect();
+ simulation.tick();
+
+ assert!(!simulation.has_component::<TicksConnected>());
+}