diff options
Diffstat (limited to 'azalea-client/tests/simulation')
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>()); +} |
