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

start updating to 25w19a and add patches to make pumpkin extractor work on snapshots

This commit is contained in:
mat 2025-05-11 02:29:04 -14:00
commit 023260833c
116 changed files with 11810 additions and 8999 deletions

42
CHANGELOG.md Normal file
View file

@ -0,0 +1,42 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Due to the complexity of Azalea and the fact that almost every Minecraft version
is breaking anyways, semantic versioning is not followed.
## [Unreleased]
### Added
- This changelog. To see changes before this update, look at the git commits.
- azalea and azalea-client now have a `packet-event` feature, which can be disabled for efficiency if you're not using `Event::Packet`.
- `StartJoinServerEvent` can now be used to join servers exclusively from the ECS without a Tokio runtime.
- `FormattedText::to_html` and `FormattedText::to_custom_format`. (@Kumpelinus)
- Non-standard legacy hex colors like `§#ff0000` are now supported in azalea-chat.
- Chat signing.
- Add auto-reconnecting which is enabled by default.
- `client.start_use_item()`.
- The pathfinder no longer avoids slabs, stairs, and dirt path blocks.
- The reach distance for the pathfinder `ReachBlockPosGoal` is now configurable. (@x-osc)
### Changed
- [BREAKING] `Client::goto` is now async and completes when the client reaches its destination. `Client::start_goto` should be used if the old behavior is desired.
- [BREAKING] The `BlockState::id` field is now private, use `.id()` instead.
- [BREAKING] Update to [Bevy 0.16](https://bevyengine.org/news/bevy-0-16/).
- [BREAKING] Rename `InstanceContainer::insert` to `get_or_insert`.
- [BREAKING] Replace `BlockInteractEvent` with the more general-purpose `StartUseItemEvent`.
- `ClientBuilder` and `SwarmBuilder` are now Send.
### Fixed
- Clients now validate incoming packets using the correct `MAXIMUM_UNCOMPRESSED_LENGTH` value.
- Several protocol fixes, including for ClientboundSetPlayerTeam and a few data components.
- No more chunk errors when the client joins another world with the same name but different height.
- Mining now aborts correctly and doesn't flag Grim.
- Update the `InstanceName` component correctly when we receive a respawn or second login packet.
- azalea-chat now handles legacy color codes correctly when parsing from NBT.
- Send the correct UUID to servers in `ClientboundHello` when we're joining in offline-mode.
- Block shapes and some properties were using data from `1.20.3-pre4` due to using an old data generator (Pixlyzer), which has now been replaced with the data generator from [Pumpkin](https://github.com/Pumpkin-MC/Extractor).

541
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -30,14 +30,14 @@ repository = "https://github.com/azalea-rs/azalea"
[workspace.dependencies]
aes = "0.8.4"
anyhow = "1.0.97"
anyhow = "1.0.98"
async-recursion = "1.1.1"
base64 = "0.22.1"
bevy_app = "0.15.3"
bevy_ecs = { version = "0.15.3", default-features = false }
bevy_log = "0.15.3"
bevy_tasks = "0.15.3"
bevy_time = "0.15.3"
bevy_app = "0.16.0"
bevy_ecs = { version = "0.16.0", default-features = false }
bevy_log = "0.16.0"
bevy_tasks = "0.16.0"
bevy_time = "0.16.0"
byteorder = "1.5.0"
cfb8 = "0.8.1"
chrono = { version = "0.4.40", default-features = false }
@ -54,7 +54,7 @@ nohash-hasher = "0.2.0"
num-bigint = "0.4.6"
num-traits = "0.2.19"
parking_lot = "0.12.3"
proc-macro2 = "1.0.94"
proc-macro2 = "1.0.95"
quote = "1.0.40"
rand = "0.8.4"
regex = "1.11.1"
@ -71,11 +71,11 @@ socks5-impl = "0.6.2"
syn = "2.0.100"
thiserror = "2.0.12"
tokio = "1.44.2"
tokio-util = "0.7.14"
tokio-util = "0.7.15"
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
hickory-resolver = { version = "0.25.1", default-features = false }
uuid = "1.12"
hickory-resolver = "0.25.2"
uuid = "1.16"
num-format = "0.4.4"
indexmap = "2.9.0"
paste = "1.0.15"

View file

@ -2036,10 +2036,10 @@ make_block_states! {
acacia_planks => BlockBehavior::new().strength(2.0, 3.0), {},
cherry_planks => BlockBehavior::new().strength(2.0, 3.0), {},
dark_oak_planks => BlockBehavior::new().strength(2.0, 3.0), {},
pale_oak_wood => BlockBehavior::new(), {
pale_oak_wood => BlockBehavior::new().strength(2.0, 2.0), {
"axis": Axis::Y,
},
pale_oak_planks => BlockBehavior::new(), {},
pale_oak_planks => BlockBehavior::new().strength(2.0, 3.0), {},
mangrove_planks => BlockBehavior::new().strength(2.0, 3.0), {},
bamboo_planks => BlockBehavior::new().strength(2.0, 3.0), {},
bamboo_mosaic => BlockBehavior::new().strength(2.0, 3.0), {},
@ -2117,7 +2117,7 @@ make_block_states! {
dark_oak_log => BlockBehavior::new().strength(2.0, 2.0), {
"axis": Axis::Y,
},
pale_oak_log => BlockBehavior::new(), {
pale_oak_log => BlockBehavior::new().strength(2.0, 2.0), {
"axis": Axis::Y,
},
mangrove_log => BlockBehavior::new().strength(2.0, 2.0), {
@ -2150,7 +2150,7 @@ make_block_states! {
stripped_dark_oak_log => BlockBehavior::new().strength(2.0, 2.0), {
"axis": Axis::Y,
},
stripped_pale_oak_log => BlockBehavior::new(), {
stripped_pale_oak_log => BlockBehavior::new().strength(2.0, 2.0), {
"axis": Axis::Y,
},
stripped_oak_log => BlockBehavior::new().strength(2.0, 2.0), {
@ -2207,7 +2207,7 @@ make_block_states! {
stripped_dark_oak_wood => BlockBehavior::new().strength(2.0, 2.0), {
"axis": Axis::Y,
},
stripped_pale_oak_wood => BlockBehavior::new(), {
stripped_pale_oak_wood => BlockBehavior::new().strength(2.0, 2.0), {
"axis": Axis::Y,
},
stripped_mangrove_wood => BlockBehavior::new().strength(2.0, 2.0), {
@ -2248,7 +2248,7 @@ make_block_states! {
"persistent": Persistent(false),
"waterlogged": Waterlogged(false),
},
pale_oak_leaves => BlockBehavior::new(), {
pale_oak_leaves => BlockBehavior::new().strength(0.2, 0.2), {
"distance": PaleOakLeavesDistance::_7,
"persistent": Persistent(false),
"waterlogged": Waterlogged(false),
@ -2468,7 +2468,7 @@ make_block_states! {
},
soul_fire => BlockBehavior::new(), {},
spawner => BlockBehavior::new().requires_correct_tool_for_drops().strength(5.0, 5.0), {},
creaking_heart => BlockBehavior::new(), {
creaking_heart => BlockBehavior::new().strength(10.0, 10.0), {
"axis": Axis::Y,
"creaking_heart_state": CreakingHeartState::Uprooted,
"natural": Natural(false),
@ -2533,7 +2533,7 @@ make_block_states! {
"rotation": DarkOakSignRotation::_0,
"waterlogged": Waterlogged(false),
},
pale_oak_sign => BlockBehavior::new().force_solid(true), {
pale_oak_sign => BlockBehavior::new().strength(1.0, 1.0).force_solid(true), {
"rotation": PaleOakSignRotation::_0,
"waterlogged": Waterlogged(false),
},
@ -2594,7 +2594,7 @@ make_block_states! {
"facing": FacingCardinal::North,
"waterlogged": Waterlogged(false),
},
pale_oak_wall_sign => BlockBehavior::new().force_solid(true), {
pale_oak_wall_sign => BlockBehavior::new().strength(1.0, 1.0).force_solid(true), {
"facing": FacingCardinal::North,
"waterlogged": Waterlogged(false),
},
@ -2641,7 +2641,7 @@ make_block_states! {
"rotation": DarkOakHangingSignRotation::_0,
"waterlogged": Waterlogged(false),
},
pale_oak_hanging_sign => BlockBehavior::new().force_solid(true), {
pale_oak_hanging_sign => BlockBehavior::new().strength(1.0, 1.0).force_solid(true), {
"attached": Attached(false),
"rotation": PaleOakHangingSignRotation::_0,
"waterlogged": Waterlogged(false),
@ -2694,7 +2694,7 @@ make_block_states! {
"facing": FacingCardinal::North,
"waterlogged": Waterlogged(false),
},
pale_oak_wall_hanging_sign => BlockBehavior::new().force_solid(true), {
pale_oak_wall_hanging_sign => BlockBehavior::new().strength(1.0, 1.0).force_solid(true), {
"facing": FacingCardinal::North,
"waterlogged": Waterlogged(false),
},
@ -2719,10 +2719,10 @@ make_block_states! {
"facing": FacingCardinal::North,
"powered": Powered(false),
},
stone_pressure_plate => BlockBehavior::new().requires_correct_tool_for_drops().strength(0.5, 0.5).force_solid(true), {
stone_pressure_plate => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"powered": Powered(false),
},
iron_door => BlockBehavior::new().requires_correct_tool_for_drops().strength(5.0, 5.0), {
iron_door => BlockBehavior::new().strength(5.0, 5.0), {
"facing": FacingCardinal::North,
"half": Half::Lower,
"hinge": Hinge::Left,
@ -2750,7 +2750,7 @@ make_block_states! {
dark_oak_pressure_plate => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"powered": Powered(false),
},
pale_oak_pressure_plate => BlockBehavior::new().force_solid(true), {
pale_oak_pressure_plate => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"powered": Powered(false),
},
mangrove_pressure_plate => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
@ -2897,7 +2897,7 @@ make_block_states! {
"powered": Powered(false),
"waterlogged": Waterlogged(false),
},
pale_oak_trapdoor => BlockBehavior::new(), {
pale_oak_trapdoor => BlockBehavior::new().strength(3.0, 3.0), {
"facing": FacingCardinal::North,
"half": TopBottom::Bottom,
"open": Open(false),
@ -3040,18 +3040,18 @@ make_block_states! {
},
lily_pad => BlockBehavior::new(), {},
resin_block => BlockBehavior::new(), {},
resin_bricks => BlockBehavior::new(), {},
resin_brick_stairs => BlockBehavior::new(), {
resin_bricks => BlockBehavior::new().requires_correct_tool_for_drops().strength(1.5, 6.0), {},
resin_brick_stairs => BlockBehavior::new().requires_correct_tool_for_drops().strength(1.5, 6.0), {
"facing": FacingCardinal::North,
"half": TopBottom::Bottom,
"shape": StairShape::Straight,
"waterlogged": Waterlogged(false),
},
resin_brick_slab => BlockBehavior::new(), {
resin_brick_slab => BlockBehavior::new().requires_correct_tool_for_drops().strength(1.5, 6.0), {
"type": Type::Bottom,
"waterlogged": Waterlogged(false),
},
resin_brick_wall => BlockBehavior::new(), {
resin_brick_wall => BlockBehavior::new().requires_correct_tool_for_drops().strength(1.5, 6.0), {
"east": WallEast::None,
"north": WallNorth::None,
"south": WallSouth::None,
@ -3059,7 +3059,7 @@ make_block_states! {
"waterlogged": Waterlogged(false),
"west": WallWest::None,
},
chiseled_resin_bricks => BlockBehavior::new(), {},
chiseled_resin_bricks => BlockBehavior::new().requires_correct_tool_for_drops().strength(1.5, 6.0), {},
nether_bricks => BlockBehavior::new().requires_correct_tool_for_drops().strength(2.0, 6.0), {},
nether_brick_fence => BlockBehavior::new().requires_correct_tool_for_drops().strength(2.0, 6.0), {
"east": East(false),
@ -3078,7 +3078,7 @@ make_block_states! {
"age": NetherWartAge::_0,
},
enchanting_table => BlockBehavior::new().requires_correct_tool_for_drops().strength(5.0, 1200.0), {},
brewing_stand => BlockBehavior::new().requires_correct_tool_for_drops().strength(0.5, 0.5), {
brewing_stand => BlockBehavior::new().strength(0.5, 0.5), {
"has_bottle_0": HasBottle0(false),
"has_bottle_1": HasBottle1(false),
"has_bottle_2": HasBottle2(false),
@ -3113,7 +3113,7 @@ make_block_states! {
},
emerald_ore => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.0, 3.0), {},
deepslate_emerald_ore => BlockBehavior::new().requires_correct_tool_for_drops().strength(4.5, 3.0), {},
ender_chest => BlockBehavior::new().requires_correct_tool_for_drops().strength(22.5, 600.0), {
ender_chest => BlockBehavior::new().strength(22.5, 600.0), {
"facing": FacingCardinal::North,
"waterlogged": Waterlogged(false),
},
@ -3241,7 +3241,7 @@ make_block_states! {
"facing": FacingCardinal::North,
"powered": Powered(false),
},
pale_oak_button => BlockBehavior::new(), {
pale_oak_button => BlockBehavior::new().strength(0.5, 0.5), {
"face": Face::Wall,
"facing": FacingCardinal::North,
"powered": Powered(false),
@ -3326,10 +3326,10 @@ make_block_states! {
"facing": FacingCardinal::North,
"waterlogged": Waterlogged(false),
},
light_weighted_pressure_plate => BlockBehavior::new().requires_correct_tool_for_drops().strength(0.5, 0.5).force_solid(true), {
light_weighted_pressure_plate => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"power": LightWeightedPressurePlatePower::_0,
},
heavy_weighted_pressure_plate => BlockBehavior::new().requires_correct_tool_for_drops().strength(0.5, 0.5).force_solid(true), {
heavy_weighted_pressure_plate => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"power": HeavyWeightedPressurePlatePower::_0,
},
comparator => BlockBehavior::new(), {
@ -3513,7 +3513,7 @@ make_block_states! {
"shape": StairShape::Straight,
"waterlogged": Waterlogged(false),
},
pale_oak_stairs => BlockBehavior::new(), {
pale_oak_stairs => BlockBehavior::new().strength(2.0, 3.0), {
"facing": FacingCardinal::North,
"half": TopBottom::Bottom,
"shape": StairShape::Straight,
@ -3759,7 +3759,7 @@ make_block_states! {
"type": Type::Bottom,
"waterlogged": Waterlogged(false),
},
pale_oak_slab => BlockBehavior::new(), {
pale_oak_slab => BlockBehavior::new().strength(2.0, 3.0), {
"type": Type::Bottom,
"waterlogged": Waterlogged(false),
},
@ -3871,7 +3871,7 @@ make_block_states! {
"open": Open(false),
"powered": Powered(false),
},
pale_oak_fence_gate => BlockBehavior::new().force_solid(true), {
pale_oak_fence_gate => BlockBehavior::new().strength(2.0, 3.0).force_solid(true), {
"facing": FacingCardinal::North,
"in_wall": InWall(false),
"open": Open(false),
@ -3931,7 +3931,7 @@ make_block_states! {
"waterlogged": Waterlogged(false),
"west": West(false),
},
pale_oak_fence => BlockBehavior::new(), {
pale_oak_fence => BlockBehavior::new().strength(2.0, 3.0), {
"east": East(false),
"north": North(false),
"south": South(false),
@ -3994,7 +3994,7 @@ make_block_states! {
"open": Open(false),
"powered": Powered(false),
},
pale_oak_door => BlockBehavior::new(), {
pale_oak_door => BlockBehavior::new().strength(3.0, 3.0), {
"facing": FacingCardinal::North,
"half": Half::Lower,
"hinge": Hinge::Left,
@ -4220,7 +4220,7 @@ make_block_states! {
sniffer_egg => BlockBehavior::new().strength(0.5, 0.5), {
"hatch": SnifferEggHatch::_0,
},
dried_ghast => BlockBehavior::new(), {
dried_ghast => BlockBehavior::new().force_solid(true), {
"facing": FacingCardinal::North,
"hydration": DriedGhastHydration::_0,
"waterlogged": Waterlogged(false),
@ -4630,16 +4630,16 @@ make_block_states! {
stonecutter => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.5, 3.5), {
"facing": FacingCardinal::North,
},
bell => BlockBehavior::new().requires_correct_tool_for_drops().strength(5.0, 5.0).force_solid(true), {
bell => BlockBehavior::new().strength(5.0, 5.0).force_solid(true), {
"attachment": Attachment::Floor,
"facing": FacingCardinal::North,
"powered": Powered(false),
},
lantern => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.5, 3.5).force_solid(true), {
lantern => BlockBehavior::new().strength(3.5, 3.5).force_solid(true), {
"hanging": Hanging(false),
"waterlogged": Waterlogged(false),
},
soul_lantern => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.5, 3.5).force_solid(true), {
soul_lantern => BlockBehavior::new().strength(3.5, 3.5).force_solid(true), {
"hanging": Hanging(false),
"waterlogged": Waterlogged(false),
},
@ -4813,10 +4813,10 @@ make_block_states! {
jigsaw => BlockBehavior::new().requires_correct_tool_for_drops().strength(-1.0, 3600000.0), {
"orientation": Orientation::NorthUp,
},
test_block => BlockBehavior::new(), {
test_block => BlockBehavior::new().strength(-1.0, 3600000.0), {
"mode": TestMode::Start,
},
test_instance_block => BlockBehavior::new(), {},
test_instance_block => BlockBehavior::new().strength(-1.0, 3600000.0), {},
composter => BlockBehavior::new().strength(0.6, 0.6), {
"level": ComposterLevel::_0,
},
@ -4896,7 +4896,7 @@ make_block_states! {
"type": Type::Bottom,
"waterlogged": Waterlogged(false),
},
polished_blackstone_pressure_plate => BlockBehavior::new().requires_correct_tool_for_drops().strength(0.5, 0.5).force_solid(true), {
polished_blackstone_pressure_plate => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"powered": Powered(false),
},
polished_blackstone_button => BlockBehavior::new().strength(0.5, 0.5), {
@ -5000,55 +5000,55 @@ make_block_states! {
"lit": Lit(false),
"waterlogged": Waterlogged(false),
},
candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
white_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
white_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
orange_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
orange_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
magenta_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
magenta_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
light_blue_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
light_blue_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
yellow_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
yellow_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
lime_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
lime_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
pink_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
pink_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
gray_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
gray_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
light_gray_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
light_gray_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
cyan_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
cyan_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
purple_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
purple_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
blue_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
blue_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
brown_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
brown_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
green_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
green_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
red_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
red_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
black_candle_cake => BlockBehavior::new().strength(0.5, 0.5), {
black_candle_cake => BlockBehavior::new().strength(0.5, 0.5).force_solid(true), {
"lit": Lit(false),
},
amethyst_block => BlockBehavior::new().requires_correct_tool_for_drops().strength(1.5, 1.5), {},
@ -5057,15 +5057,15 @@ make_block_states! {
"facing": FacingCubic::Up,
"waterlogged": Waterlogged(false),
},
large_amethyst_bud => BlockBehavior::new().strength(1.5, 1.5), {
large_amethyst_bud => BlockBehavior::new().strength(1.5, 1.5).force_solid(true), {
"facing": FacingCubic::Up,
"waterlogged": Waterlogged(false),
},
medium_amethyst_bud => BlockBehavior::new().strength(1.5, 1.5), {
medium_amethyst_bud => BlockBehavior::new().strength(1.5, 1.5).force_solid(true), {
"facing": FacingCubic::Up,
"waterlogged": Waterlogged(false),
},
small_amethyst_bud => BlockBehavior::new().strength(1.5, 1.5), {
small_amethyst_bud => BlockBehavior::new().strength(1.5, 1.5).force_solid(true), {
"facing": FacingCubic::Up,
"waterlogged": Waterlogged(false),
},
@ -5266,56 +5266,56 @@ make_block_states! {
"type": Type::Bottom,
"waterlogged": Waterlogged(false),
},
copper_door => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.0, 6.0), {
copper_door => BlockBehavior::new().strength(3.0, 6.0), {
"facing": FacingCardinal::North,
"half": Half::Lower,
"hinge": Hinge::Left,
"open": Open(false),
"powered": Powered(false),
},
exposed_copper_door => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.0, 6.0), {
exposed_copper_door => BlockBehavior::new().strength(3.0, 6.0), {
"facing": FacingCardinal::North,
"half": Half::Lower,
"hinge": Hinge::Left,
"open": Open(false),
"powered": Powered(false),
},
oxidized_copper_door => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.0, 6.0), {
oxidized_copper_door => BlockBehavior::new().strength(3.0, 6.0), {
"facing": FacingCardinal::North,
"half": Half::Lower,
"hinge": Hinge::Left,
"open": Open(false),
"powered": Powered(false),
},
weathered_copper_door => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.0, 6.0), {
weathered_copper_door => BlockBehavior::new().strength(3.0, 6.0), {
"facing": FacingCardinal::North,
"half": Half::Lower,
"hinge": Hinge::Left,
"open": Open(false),
"powered": Powered(false),
},
waxed_copper_door => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.0, 6.0), {
waxed_copper_door => BlockBehavior::new().strength(3.0, 6.0), {
"facing": FacingCardinal::North,
"half": Half::Lower,
"hinge": Hinge::Left,
"open": Open(false),
"powered": Powered(false),
},
waxed_exposed_copper_door => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.0, 6.0), {
waxed_exposed_copper_door => BlockBehavior::new().strength(3.0, 6.0), {
"facing": FacingCardinal::North,
"half": Half::Lower,
"hinge": Hinge::Left,
"open": Open(false),
"powered": Powered(false),
},
waxed_oxidized_copper_door => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.0, 6.0), {
waxed_oxidized_copper_door => BlockBehavior::new().strength(3.0, 6.0), {
"facing": FacingCardinal::North,
"half": Half::Lower,
"hinge": Hinge::Left,
"open": Open(false),
"powered": Powered(false),
},
waxed_weathered_copper_door => BlockBehavior::new().requires_correct_tool_for_drops().strength(3.0, 6.0), {
waxed_weathered_copper_door => BlockBehavior::new().strength(3.0, 6.0), {
"facing": FacingCardinal::North,
"half": Half::Lower,
"hinge": Hinge::Left,
@ -5600,20 +5600,20 @@ make_block_states! {
"orientation": Orientation::NorthUp,
"triggered": Triggered(false),
},
trial_spawner => BlockBehavior::new().requires_correct_tool_for_drops().strength(50.0, 50.0), {
trial_spawner => BlockBehavior::new().strength(50.0, 50.0), {
"ominous": Ominous(false),
"trial_spawner_state": TrialSpawnerState::Inactive,
},
vault => BlockBehavior::new(), {
vault => BlockBehavior::new().strength(50.0, 50.0), {
"facing": FacingCardinal::North,
"ominous": Ominous(false),
"vault_state": VaultState::Inactive,
},
heavy_core => BlockBehavior::new(), {
heavy_core => BlockBehavior::new().strength(10.0, 1200.0), {
"waterlogged": Waterlogged(false),
},
pale_moss_block => BlockBehavior::new(), {},
pale_moss_carpet => BlockBehavior::new(), {
pale_moss_block => BlockBehavior::new().strength(0.1, 0.1), {},
pale_moss_carpet => BlockBehavior::new().strength(0.1, 0.1), {
"bottom": Bottom(true),
"east": WallEast::None,
"north": WallNorth::None,

View file

@ -467,7 +467,7 @@ impl<S> CommandDispatcher<S> {
Ordering::Equal => {
let usage = child_usage.into_iter().next().unwrap();
let usage = if child_optional {
format!("[{}]", usage)
format!("[{usage}]")
} else {
usage
};

View file

@ -7,12 +7,7 @@ use azalea_brigadier::{
context::CommandContext,
};
use bevy_app::App;
use bevy_ecs::{
component::Component,
query::With,
system::{Query, Resource, RunSystemOnce},
world::{FromWorld, World},
};
use bevy_ecs::{prelude::*, system::RunSystemOnce};
use parking_lot::Mutex;
#[test]

View file

@ -67,6 +67,105 @@ impl FormattedText {
}
}
/// Render all components into a single `String`, using your custom
/// closures to drive styling, text transformation, and final cleanup.
///
/// # Type params
/// - `F`: `(running, component, default) -> (prefix, suffix)` for
/// per-component styling
/// - `S`: `&str -> String` for text tweaks (escaping, mapping, etc.)
/// - `C`: `&final_running_style -> String` for any trailing cleanup
///
/// # Args
/// - `style_formatter`: how to open/close each components style
/// - `text_formatter`: how to turn raw text into output text
/// - `cleanup_formatter`: emit after all components (e.g. reset codes)
/// - `default_style`: where to reset when a components `reset` is true
///
/// # Example
/// ```rust
/// use azalea_chat::{FormattedText, DEFAULT_STYLE};
/// use serde::de::Deserialize;
///
/// let component = FormattedText::deserialize(&serde_json::json!({
/// "text": "Hello, world!",
/// "color": "red",
/// })).unwrap();
///
/// let ansi = component.to_custom_format(
/// |running, new, default| (running.compare_ansi(new, default), String::new()),
/// |text| text.to_string(),
/// |style| {
/// if !style.is_empty() {
/// "\u{1b}[m".to_string()
/// } else {
/// String::new()
/// }
/// },
/// &DEFAULT_STYLE,
/// );
/// println!("{}", ansi);
/// ```
pub fn to_custom_format<F, S, C>(
&self,
mut style_formatter: F,
mut text_formatter: S,
mut cleanup_formatter: C,
default_style: &Style,
) -> String
where
F: FnMut(&Style, &Style, &Style) -> (String, String),
S: FnMut(&str) -> String,
C: FnMut(&Style) -> String,
{
let mut output = String::new();
let mut running_style = Style::default();
for component in self.clone().into_iter() {
let component_text = match &component {
Self::Text(c) => c.text.to_string(),
Self::Translatable(c) => match c.read() {
Ok(c) => c.to_string(),
Err(_) => c.key.to_string(),
},
};
let component_style = &component.get_base().style;
let formatted_style = style_formatter(&running_style, component_style, default_style);
let formatted_text = text_formatter(&component_text);
output.push_str(&formatted_style.0);
output.push_str(&formatted_text);
output.push_str(&formatted_style.1);
// Reset running style if required
if component_style.reset {
running_style = default_style.clone();
} else {
running_style.apply(component_style);
}
}
output.push_str(&cleanup_formatter(&running_style));
output
}
/// Convert this component into an
/// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code).
///
/// This is the same as [`FormattedText::to_ansi`], but you can specify a
/// default [`Style`] to use.
pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
self.to_custom_format(
|running, new, default| (running.compare_ansi(new, default), "".to_owned()),
|text| text.to_string(),
|style| if !style.is_empty() { "\u{1b}[m" } else { "" }.to_string(),
default_style,
)
}
/// Convert this component into an
/// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code), so you
/// can print it to your terminal and get styling.
@ -89,41 +188,30 @@ impl FormattedText {
/// println!("{}", component.to_ansi());
/// ```
pub fn to_ansi(&self) -> String {
// default the default_style to white if it's not set
self.to_ansi_with_custom_style(&DEFAULT_STYLE)
}
/// Convert this component into an
/// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code).
///
/// This is the same as [`FormattedText::to_ansi`], but you can specify a
/// default [`Style`] to use.
pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
// this contains the final string will all the ansi escape codes
let mut built_string = String::new();
// this style will update as we visit components
let mut running_style = Style::default();
for component in self.clone().into_iter() {
let component_text = match &component {
Self::Text(c) => c.text.to_string(),
Self::Translatable(c) => c.to_string(),
};
let component_style = &component.get_base().style;
let ansi_text = running_style.compare_ansi(component_style, default_style);
built_string.push_str(&ansi_text);
built_string.push_str(&component_text);
running_style.apply(component_style);
}
if !running_style.is_empty() {
built_string.push_str("\u{1b}[m");
}
built_string
pub fn to_html(&self) -> String {
self.to_custom_format(
|running, new, _| {
(
format!(
"<span style=\"{}\">",
running.merged_with(new).get_html_style()
),
"</span>".to_owned(),
)
},
|text| {
text.replace("&", "&amp;")
.replace("<", "&lt;")
// usually unnecessary but good for compatibility
.replace(">", "&gt;")
.replace("\n", "<br>")
},
|_| "".to_string(),
&DEFAULT_STYLE,
)
}
}
@ -441,8 +529,9 @@ impl FormattedText {
component.append(FormattedText::from_nbt_tag(extra)?);
}
let style = Style::from_compound(compound).ok()?;
component.get_base_mut().style = style;
let base_style = Style::from_compound(compound).ok()?;
let new_style = &mut component.get_base_mut().style;
*new_style = new_style.merged_with(&base_style);
Some(component)
}

View file

@ -8,4 +8,4 @@ pub mod style;
pub mod text_component;
pub mod translatable_component;
pub use component::FormattedText;
pub use component::{DEFAULT_STYLE, FormattedText};

View file

@ -559,6 +559,21 @@ impl Style {
}
}
/// Returns a new style that is a merge of self and other.
/// For any field that `other` does not specify (is None), selfs value is
/// used.
pub fn merged_with(&self, other: &Style) -> Style {
Style {
color: other.color.clone().or(self.color.clone()),
bold: other.bold.or(self.bold),
italic: other.italic.or(self.italic),
underlined: other.underlined.or(self.underlined),
strikethrough: other.strikethrough.or(self.strikethrough),
obfuscated: other.obfuscated.or(self.obfuscated),
reset: other.reset, // if reset is true in the new style, that takes precedence
}
}
/// Apply a ChatFormatting to this style
pub fn apply_formatting(&mut self, formatting: &ChatFormatting) {
match *formatting {
@ -576,6 +591,48 @@ impl Style {
}
}
}
pub fn get_html_style(&self) -> String {
let mut style = String::new();
if let Some(color) = &self.color {
style.push_str(&format!("color: {};", color.format_value()));
}
if let Some(bold) = self.bold {
style.push_str(&format!(
"font-weight: {};",
if bold { "bold" } else { "normal" }
));
}
if let Some(italic) = self.italic {
style.push_str(&format!(
"font-style: {};",
if italic { "italic" } else { "normal" }
));
}
if let Some(underlined) = self.underlined {
style.push_str(&format!(
"text-decoration: {};",
if underlined { "underline" } else { "none" }
));
}
if let Some(strikethrough) = self.strikethrough {
style.push_str(&format!(
"text-decoration: {};",
if strikethrough {
"line-through"
} else {
"none"
}
));
}
if let Some(obfuscated) = self.obfuscated {
if obfuscated {
style.push_str("filter: blur(2px);");
}
}
style
}
}
#[cfg(feature = "simdnbt")]

View file

@ -2,7 +2,11 @@ use std::fmt::Display;
use serde::{__private::ser::FlatMapSerializer, Serialize, Serializer, ser::SerializeMap};
use crate::{FormattedText, base_component::BaseComponent, style::ChatFormatting};
use crate::{
FormattedText,
base_component::BaseComponent,
style::{ChatFormatting, TextColor},
};
/// A component that contains text that's the same in all locales.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
@ -69,13 +73,26 @@ pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextCompo
i += 1;
continue;
};
if let Some(formatter) = ChatFormatting::from_code(formatting_code) {
if formatting_code == '#' {
let color = legacy_color_code
.chars()
.skip(i + 1)
// 7 to include the #
.take(7)
.collect::<String>();
if components.is_empty() || !components.last().unwrap().text.is_empty() {
components.push(TextComponent::new("".to_string()));
}
let style = &mut components.last_mut().unwrap().base.style;
style.color = TextColor::parse(color);
i += 6;
} else if let Some(formatter) = ChatFormatting::from_code(formatting_code) {
if components.is_empty() || !components.last().unwrap().text.is_empty() {
components.push(TextComponent::new("".to_string()));
}
let style = &mut components.last_mut().unwrap().base.style;
// if the formatter is a reset, then we need to reset the style to the default
style.apply_formatting(&formatter);
}
i += 1;
@ -146,7 +163,7 @@ mod tests {
use crate::style::Ansi;
#[test]
fn test_hypixel_motd() {
fn test_hypixel_motd_ansi() {
let component =
TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string())
.get();
@ -163,6 +180,39 @@ mod tests {
);
}
#[test]
fn test_hypixel_motd_html() {
let component =
TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string())
.get();
assert_eq!(
component.to_html(),
format!(
"{GREEN}Hypixel Network {END_SPAN}{RED}[1.8-1.18]<br>{END_SPAN}{BOLD_AQUA}HAPPY HOLIDAYS{END_SPAN}",
END_SPAN = "</span>",
GREEN = "<span style=\"color: #55FF55;\">",
RED = "<span style=\"color: #FF5555;\">",
BOLD_AQUA = "<span style=\"color: #55FFFF;font-weight: bold;\">",
)
);
}
#[test]
fn test_xss_html() {
let component = TextComponent::new("§a<b>&\n§b</b>".to_string()).get();
assert_eq!(
component.to_html(),
format!(
"{GREEN}&lt;b&gt;&amp;<br>{END_SPAN}{AQUA}&lt;/b&gt;{END_SPAN}",
END_SPAN = "</span>",
GREEN = "<span style=\"color: #55FF55;\">",
AQUA = "<span style=\"color: #55FFFF;\">",
)
);
}
#[test]
fn test_legacy_color_code_to_component() {
let component = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_string()).get();
@ -180,4 +230,17 @@ mod tests {
)
);
}
#[test]
fn test_legacy_color_code_with_rgb() {
let component = TextComponent::new("§#Ff0000This is a test message".to_string()).get();
assert_eq!(
component.to_ansi(),
format!(
"{RED}This is a test message{RESET}",
RED = Ansi::rgb(0xff0000),
RESET = Ansi::RESET
)
);
}
}

View file

@ -25,6 +25,7 @@ bevy_ecs.workspace = true
bevy_log = { workspace = true, optional = true }
bevy_tasks.workspace = true
bevy_time.workspace = true
chrono = { workspace = true, features = ["now"] }
derive_more = { workspace = true, features = ["deref", "deref_mut"] }
minecraft_folder_path.workspace = true
parking_lot.workspace = true

View file

@ -53,7 +53,7 @@ pub struct Account {
///
/// This is set when you call [`Self::request_certs`], but you only
/// need to if the servers you're joining require it.
pub certs: Option<Certificates>,
pub certs: Arc<Mutex<Option<Certificates>>>,
}
/// The parameters that were passed for creating the associated [`Account`].
@ -82,7 +82,7 @@ impl Account {
account_opts: AccountOpts::Offline {
username: username.to_string(),
},
certs: None,
certs: Arc::new(Mutex::new(None)),
}
}
@ -127,7 +127,7 @@ impl Account {
email: email.to_string(),
},
// we don't do chat signing by default unless the user asks for it
certs: None,
certs: Arc::new(Mutex::new(None)),
})
}
@ -194,7 +194,7 @@ impl Account {
account_opts: AccountOpts::MicrosoftWithAccessToken {
msa: Arc::new(Mutex::new(msa)),
},
certs: None,
certs: Arc::new(Mutex::new(None)),
})
}
/// Refresh the access_token for this account to be valid again.
@ -260,7 +260,7 @@ impl Account {
.lock()
.clone();
let certs = azalea_auth::certs::fetch_certificates(&access_token).await?;
self.certs = Some(certs);
*self.certs.lock() = Some(certs);
Ok(())
}

View file

@ -1,7 +1,7 @@
use std::{
collections::HashMap,
fmt::Debug,
io,
io, mem,
net::SocketAddr,
sync::Arc,
thread,
@ -15,31 +15,25 @@ use azalea_core::{
tick::GameTick,
};
use azalea_entity::{
EntityUpdateSet, EyeHeight, LocalEntity, Position,
EntityUpdateSet, EyeHeight, Position,
indexing::{EntityIdIndex, EntityUuidIndex},
metadata::Health,
};
use azalea_protocol::{
ServerAddress,
common::client_information::ClientInformation,
connect::{Connection, ConnectionError, Proxy},
connect::{ConnectionError, Proxy},
packets::{
self, ClientIntention, ConnectionProtocol, PROTOCOL_VERSION, Packet,
self, Packet,
game::{self, ServerboundGamePacket},
handshake::s_intention::ServerboundIntention,
login::s_hello::ServerboundHello,
},
resolver,
};
use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
use bevy_app::{App, Plugin, PluginsState, Update};
use bevy_app::{App, Plugin, PluginsState, SubApp, Update};
use bevy_ecs::{
bundle::Bundle,
component::Component,
entity::Entity,
schedule::{InternedScheduleLabel, IntoSystemConfigs, LogLevel, ScheduleBuildSettings},
system::{Commands, Resource},
world::World,
prelude::*,
schedule::{InternedScheduleLabel, LogLevel, ScheduleBuildSettings},
};
use parking_lot::{Mutex, RwLock};
use simdnbt::owned::NbtCompound;
@ -57,19 +51,16 @@ use crate::{
chunks::ChunkBatchInfo,
connection::RawConnection,
disconnect::DisconnectEvent,
events::{Event, LocalPlayerEvents},
events::Event,
interact::CurrentSequenceNumber,
inventory::Inventory,
join::{ConnectOpts, StartJoinCallback, StartJoinServerEvent},
local_player::{
GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList,
},
mining::{self},
movement::{LastSentLookDirection, PhysicsState},
packet::{
as_system,
game::SendPacketEvent,
login::{InLoginState, SendLoginPacketEvent},
},
packet::game::SendPacketEvent,
player::retroactively_add_game_profile_component,
};
@ -116,39 +107,40 @@ pub enum JoinError {
Disconnect { reason: FormattedText },
}
pub struct StartClientOpts<'a> {
pub struct StartClientOpts {
pub ecs_lock: Arc<Mutex<World>>,
pub account: &'a Account,
pub address: &'a ServerAddress,
pub resolved_address: &'a SocketAddr,
pub proxy: Option<Proxy>,
pub account: Account,
pub connect_opts: ConnectOpts,
pub event_sender: Option<mpsc::UnboundedSender<Event>>,
}
impl<'a> StartClientOpts<'a> {
impl StartClientOpts {
pub fn new(
account: &'a Account,
address: &'a ServerAddress,
resolved_address: &'a SocketAddr,
account: Account,
address: ServerAddress,
resolved_address: SocketAddr,
event_sender: Option<mpsc::UnboundedSender<Event>>,
) -> StartClientOpts<'a> {
) -> StartClientOpts {
let mut app = App::new();
app.add_plugins(DefaultPlugins);
let ecs_lock = start_ecs_runner(app);
let (ecs_lock, start_running_systems) = start_ecs_runner(app.main_mut());
start_running_systems();
Self {
ecs_lock,
account,
address,
resolved_address,
proxy: None,
connect_opts: ConnectOpts {
address,
resolved_address,
proxy: None,
},
event_sender,
}
}
pub fn proxy(mut self, proxy: Proxy) -> Self {
self.proxy = Some(proxy);
self.connect_opts.proxy = Some(proxy);
self
}
}
@ -181,14 +173,14 @@ impl Client {
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let account = Account::offline("bot");
/// let (client, rx) = Client::join(&account, "localhost").await?;
/// let (client, rx) = Client::join(account, "localhost").await?;
/// client.chat("Hello, world!");
/// client.disconnect();
/// Ok(())
/// }
/// ```
pub async fn join(
account: &Account,
account: Account,
address: impl TryInto<ServerAddress>,
) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
@ -197,8 +189,8 @@ impl Client {
let client = Self::start_client(StartClientOpts::new(
account,
&address,
&resolved_address,
address,
resolved_address,
Some(tx),
))
.await?;
@ -206,7 +198,7 @@ impl Client {
}
pub async fn join_with_proxy(
account: &Account,
account: Account,
address: impl TryInto<ServerAddress>,
proxy: Proxy,
) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
@ -215,7 +207,7 @@ impl Client {
let (tx, rx) = mpsc::unbounded_channel();
let client = Self::start_client(
StartClientOpts::new(account, &address, &resolved_address, Some(tx)).proxy(proxy),
StartClientOpts::new(account, address, resolved_address, Some(tx)).proxy(proxy),
)
.await?;
Ok((client, rx))
@ -227,109 +219,26 @@ impl Client {
StartClientOpts {
ecs_lock,
account,
address,
resolved_address,
proxy,
connect_opts,
event_sender,
}: StartClientOpts<'_>,
}: StartClientOpts,
) -> Result<Self, JoinError> {
// check if an entity with our uuid already exists in the ecs and if so then
// just use that
let entity = {
let mut ecs = ecs_lock.lock();
// send a StartJoinServerEvent
let entity_uuid_index = ecs.resource::<EntityUuidIndex>();
let uuid = account.uuid_or_offline();
let entity = if let Some(entity) = entity_uuid_index.get(&account.uuid_or_offline()) {
debug!("Reusing entity {entity:?} for client");
entity
} else {
let entity = ecs.spawn_empty().id();
debug!("Created new entity {entity:?} for client");
// add to the uuid index
let mut entity_uuid_index = ecs.resource_mut::<EntityUuidIndex>();
entity_uuid_index.insert(uuid, entity);
entity
};
let (start_join_callback_tx, mut start_join_callback_rx) =
mpsc::unbounded_channel::<Result<Entity, JoinError>>();
let mut entity_mut = ecs.entity_mut(entity);
entity_mut.insert((
InLoginState,
// add the Account to the entity now so plugins can access it earlier
account.to_owned(),
// localentity is always present for our clients, even if we're not actually logged
// in
LocalEntity,
));
if let Some(event_sender) = event_sender {
// this is optional so we don't leak memory in case the user doesn't want to
// handle receiving packets
entity_mut.insert(LocalPlayerEvents(event_sender));
}
entity
};
let mut conn = if let Some(proxy) = proxy {
Connection::new_with_proxy(resolved_address, proxy).await?
} else {
Connection::new(resolved_address).await?
};
debug!("Created connection to {resolved_address:?}");
conn.write(ServerboundIntention {
protocol_version: PROTOCOL_VERSION,
hostname: address.host.clone(),
port: address.port,
intention: ClientIntention::Login,
})
.await?;
let conn = conn.login();
let (read_conn, write_conn) = conn.into_split();
let (read_conn, write_conn) = (read_conn.raw, write_conn.raw);
// insert the client into the ecs so it finishes logging in
{
let mut ecs = ecs_lock.lock();
let instance = Instance::default();
let instance_holder = crate::local_player::InstanceHolder::new(
entity,
// default to an empty world, it'll be set correctly later when we
// get the login packet
Arc::new(RwLock::new(instance)),
);
let mut entity = ecs.entity_mut(entity);
entity.insert((
// these stay when we switch to the game state
LocalPlayerBundle {
raw_connection: RawConnection::new(
read_conn,
write_conn,
ConnectionProtocol::Login,
),
client_information: crate::ClientInformation::default(),
instance_holder,
metadata: azalea_entity::metadata::PlayerMetadataBundle::default(),
},
));
}
as_system::<Commands>(&mut ecs_lock.lock(), |mut commands| {
commands.entity(entity).insert((InLoginState,));
commands.trigger(SendLoginPacketEvent::new(
entity,
ServerboundHello {
name: account.username.clone(),
// TODO: pretty sure this should generate an offline-mode uuid instead of just
// Uuid::default()
profile_id: account.uuid.unwrap_or_default(),
},
))
ecs_lock.lock().send_event(StartJoinServerEvent {
account,
connect_opts,
event_sender,
start_join_callback_tx: Some(StartJoinCallback(start_join_callback_tx)),
});
let entity = start_join_callback_rx.recv().await.expect(
"StartJoinCallback should not be dropped before sending a message, this is a bug in Azalea",
)?;
let client = Client::new(entity, ecs_lock.clone());
Ok(client)
}
@ -734,7 +643,9 @@ impl Plugin for AzaleaPlugin {
Update,
(
// add GameProfileComponent when we get an AddPlayerEvent
retroactively_add_game_profile_component.after(EntityUpdateSet::Index),
retroactively_add_game_profile_component
.after(EntityUpdateSet::Index)
.after(crate::join::handle_start_join_server_event),
),
)
.init_resource::<InstanceContainer>()
@ -742,12 +653,14 @@ impl Plugin for AzaleaPlugin {
}
}
/// Start running the ECS loop!
/// Create the ECS world, and return a function that begins running systems.
/// This exists to allow you to make last-millisecond updates to the world
/// before any systems start running.
///
/// You can create your app with `App::new()`, but don't forget to add
/// [`DefaultPlugins`].
#[doc(hidden)]
pub fn start_ecs_runner(mut app: App) -> Arc<Mutex<World>> {
pub fn start_ecs_runner(app: &mut SubApp) -> (Arc<Mutex<World>>, impl FnOnce()) {
// this block is based on Bevy's default runner:
// https://github.com/bevyengine/bevy/blob/390877cdae7a17095a75c8f9f1b4241fe5047e83/crates/bevy_app/src/schedule_runner.rs#L77-L85
if app.plugins_state() != PluginsState::Cleaned {
@ -765,14 +678,15 @@ pub fn start_ecs_runner(mut app: App) -> Arc<Mutex<World>> {
// all resources should have been added by now so we can take the ecs from the
// app
let ecs = Arc::new(Mutex::new(std::mem::take(app.world_mut())));
let ecs = Arc::new(Mutex::new(mem::take(app.world_mut())));
tokio::spawn(run_schedule_loop(
ecs.clone(),
*app.main().update_schedule.as_ref().unwrap(),
));
let ecs_clone = ecs.clone();
let outer_schedule_label = *app.update_schedule.as_ref().unwrap();
let start_running_systems = move || {
tokio::spawn(run_schedule_loop(ecs_clone, outer_schedule_label));
};
ecs
(ecs, start_running_systems)
}
async fn run_schedule_loop(ecs: Arc<Mutex<World>>, outer_schedule_label: InternedScheduleLabel) {

View file

@ -21,6 +21,9 @@ pub mod test_simulation;
pub use account::{Account, AccountOpts};
pub use azalea_protocol::common::client_information::ClientInformation;
// Re-export bevy-tasks so plugins can make sure that they're using the same
// version.
pub use bevy_tasks;
pub use client::{
Client, InConfigState, InGameState, JoinError, JoinedClientBundle, LocalPlayerBundle,
StartClientOpts, start_ecs_runner,

View file

@ -144,6 +144,22 @@ impl InstanceHolder {
))),
}
}
/// Reset the `Instance` to a new reference to an empty instance, but with
/// the same registries as the current one.
///
/// This is used by Azalea when entering the config state.
pub fn reset(&mut self) {
let registries = self.instance.read().registries.clone();
let new_instance = Instance {
registries,
..Default::default()
};
self.instance = Arc::new(RwLock::new(new_instance));
self.partial_instance.write().reset();
}
}
#[derive(Error, Debug)]

View file

@ -35,7 +35,8 @@ impl Plugin for AttackPlugin {
update_attack_strength_scale.after(PhysicsSet),
handle_attack_queued
.before(super::tick_end::game_tick_packet)
.after(super::movement::send_sprinting_if_needed),
.after(super::movement::send_sprinting_if_needed)
.before(super::movement::send_position),
)
.chain(),
);
@ -67,10 +68,10 @@ impl Client {
/// A component that indicates that this client will be attacking the given
/// entity next tick.
#[derive(Component, Clone, Debug)]
struct AttackQueued {
pub struct AttackQueued {
pub target: MinecraftEntityId,
}
fn handle_attack_queued(
pub fn handle_attack_queued(
mut commands: Commands,
mut query: Query<(
Entity,

View file

@ -0,0 +1,136 @@
//! Auto-reconnect to the server when the client is kicked.
//!
//! See [`AutoReconnectPlugin`] for more information.
use std::time::{Duration, Instant};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use super::{
disconnect::DisconnectEvent,
events::LocalPlayerEvents,
join::{ConnectOpts, ConnectionFailedEvent, StartJoinServerEvent},
};
use crate::Account;
/// The default delay that Azalea will use for reconnecting our clients. See
/// [`AutoReconnectPlugin`] for more information.
pub const DEFAULT_RECONNECT_DELAY: Duration = Duration::from_secs(5);
/// A default plugin that makes clients automatically rejoin the server when
/// they're disconnected. The reconnect delay is configurable globally or
/// per-client with the [`AutoReconnectDelay`] resource/component. Auto
/// reconnecting can be disabled by removing the resource from the ECS.
///
/// The delay defaults to [`DEFAULT_RECONNECT_DELAY`].
pub struct AutoReconnectPlugin;
impl Plugin for AutoReconnectPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(AutoReconnectDelay::new(DEFAULT_RECONNECT_DELAY))
.add_systems(
Update,
(start_rejoin_on_disconnect, rejoin_after_delay)
.chain()
.before(super::join::handle_start_join_server_event),
);
}
}
pub fn start_rejoin_on_disconnect(
mut commands: Commands,
mut disconnect_events: EventReader<DisconnectEvent>,
mut connection_failed_events: EventReader<ConnectionFailedEvent>,
auto_reconnect_delay_res: Option<Res<AutoReconnectDelay>>,
auto_reconnect_delay_query: Query<&AutoReconnectDelay>,
) {
for entity in disconnect_events
.read()
.map(|e| e.entity)
.chain(connection_failed_events.read().map(|e| e.entity))
{
let Some(delay) = get_delay(
&auto_reconnect_delay_res,
auto_reconnect_delay_query,
entity,
) else {
// no auto reconnect
continue;
};
let reconnect_after = Instant::now() + delay;
commands.entity(entity).insert(InternalReconnectAfter {
instant: reconnect_after,
});
}
}
fn get_delay(
auto_reconnect_delay_res: &Option<Res<AutoReconnectDelay>>,
auto_reconnect_delay_query: Query<&AutoReconnectDelay>,
entity: Entity,
) -> Option<Duration> {
if let Ok(c) = auto_reconnect_delay_query.get(entity) {
Some(c.delay)
} else {
auto_reconnect_delay_res.as_ref().map(|r| r.delay)
}
}
pub fn rejoin_after_delay(
mut commands: Commands,
mut join_events: EventWriter<StartJoinServerEvent>,
query: Query<(
Entity,
&InternalReconnectAfter,
&Account,
&ConnectOpts,
Option<&LocalPlayerEvents>,
)>,
) {
for (entity, reconnect_after, account, connect_opts, local_player_events) in query.iter() {
if Instant::now() >= reconnect_after.instant {
// don't keep trying to reconnect
commands.entity(entity).remove::<InternalReconnectAfter>();
// our Entity will be reused since the account has the same uuid
join_events.write(StartJoinServerEvent {
account: account.clone(),
connect_opts: connect_opts.clone(),
// not actually necessary since we're reusing the same entity and LocalPlayerEvents
// isn't removed, but this is more readable and just in case it's changed in the
// future
event_sender: local_player_events.map(|e| e.0.clone()),
start_join_callback_tx: None,
});
}
}
}
/// A resource *and* component that indicates how long to wait before
/// reconnecting when we're kicked.
///
/// Initially, it's a resource in the ECS set to 5 seconds. You can modify
/// the resource to update the global reconnect delay, or insert it as a
/// component to set the individual delay for a single client.
///
/// You can also remove this resource from the ECS to disable the default
/// auto-reconnecting behavior. Inserting the resource/component again will not
/// make clients that were already disconnected automatically reconnect.
#[derive(Resource, Component, Debug, Clone)]
pub struct AutoReconnectDelay {
pub delay: Duration,
}
impl AutoReconnectDelay {
pub fn new(delay: Duration) -> Self {
Self { delay }
}
}
/// This is inserted when we're disconnected and indicates when we'll reconnect.
///
/// This is set based on [`AutoReconnectDelay`].
#[derive(Component, Debug, Clone)]
pub struct InternalReconnectAfter {
pub instant: Instant,
}

View file

@ -1,5 +1,6 @@
use std::time::{SystemTime, UNIX_EPOCH};
use azalea_crypto::SignChatMessageOptions;
use azalea_protocol::packets::{
Packet,
game::{ServerboundChat, ServerboundChatCommand, s_chat::LastSeenMessagesUpdate},
@ -7,7 +8,7 @@ use azalea_protocol::packets::{
use bevy_ecs::prelude::*;
use super::ChatKind;
use crate::packet::game::SendPacketEvent;
use crate::{Account, chat_signing::ChatSigningSession, packet::game::SendPacketEvent};
/// Send a chat packet to the server of a specific kind (chat message or
/// command). Usually you just want [`SendChatEvent`] instead.
@ -30,6 +31,7 @@ pub struct SendChatKindEvent {
pub fn handle_send_chat_kind_event(
mut events: EventReader<SendChatKindEvent>,
mut commands: Commands,
mut query: Query<(&Account, &mut ChatSigningSession)>,
) {
for event in events.read() {
let content = event
@ -38,22 +40,43 @@ pub fn handle_send_chat_kind_event(
.filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | '§'))
.take(256)
.collect::<String>();
let timestamp = SystemTime::now();
let packet = match event.kind {
ChatKind::Message => ServerboundChat {
message: content,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
signature: None,
last_seen_messages: LastSeenMessagesUpdate::default(),
ChatKind::Message => {
let salt = azalea_crypto::make_salt();
let signature = if let Ok((account, mut chat_session)) = query.get_mut(event.entity)
{
Some(create_signature(
account,
&mut chat_session,
salt,
timestamp,
&content,
))
} else {
None
};
ServerboundChat {
message: content,
timestamp: timestamp
.duration_since(UNIX_EPOCH)
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt,
signature,
// TODO: implement last_seen_messages
last_seen_messages: LastSeenMessagesUpdate::default(),
}
}
.into_variant(),
ChatKind::Command => {
// TODO: chat signing
// TODO: commands that require chat signing
ServerboundChatCommand { command: content }.into_variant()
}
};
@ -61,3 +84,28 @@ pub fn handle_send_chat_kind_event(
commands.trigger(SendPacketEvent::new(event.entity, packet));
}
}
pub fn create_signature(
account: &Account,
chat_session: &mut ChatSigningSession,
salt: u64,
timestamp: SystemTime,
message: &str,
) -> azalea_crypto::MessageSignature {
let certs = account.certs.lock();
let certs = certs.as_ref().expect("certs shouldn't be set back to None");
let signature = azalea_crypto::sign_chat_message(&SignChatMessageOptions {
account_uuid: account.uuid.expect("account must have a uuid"),
chat_session_uuid: chat_session.session_id,
message_index: chat_session.messages_sent,
salt,
timestamp,
message: message.to_owned(),
private_key: certs.private_key.clone(),
});
chat_session.messages_sent += 1;
signature
}

View file

@ -10,12 +10,7 @@ use azalea_protocol::packets::game::{
c_system_chat::ClientboundSystemChat,
};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::{
entity::Entity,
event::{EventReader, EventWriter},
prelude::Event,
schedule::IntoSystemConfigs,
};
use bevy_ecs::prelude::*;
use handler::{SendChatKindEvent, handle_send_chat_kind_event};
use uuid::Uuid;
@ -204,13 +199,13 @@ pub fn handle_send_chat_event(
) {
for event in events.read() {
if event.content.starts_with('/') {
send_chat_kind_events.send(SendChatKindEvent {
send_chat_kind_events.write(SendChatKindEvent {
entity: event.entity,
content: event.content[1..].to_string(),
kind: ChatKind::Command,
});
} else {
send_chat_kind_events.send(SendChatKindEvent {
send_chat_kind_events.write(SendChatKindEvent {
entity: event.entity,
content: event.content.clone(),
kind: ChatKind::Message,

View file

@ -0,0 +1,186 @@
use std::time::{Duration, Instant};
use azalea_auth::certs::{Certificates, FetchCertificatesError};
use azalea_protocol::packets::game::{
ServerboundChatSessionUpdate,
s_chat_session_update::{ProfilePublicKeyData, RemoteChatSessionData},
};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_tasks::{IoTaskPool, Task, futures_lite::future};
use chrono::Utc;
use tracing::{debug, error};
use uuid::Uuid;
use super::{chat, login::IsAuthenticated, packet::game::SendPacketEvent};
use crate::{Account, InGameState};
pub struct ChatSigningPlugin;
impl Plugin for ChatSigningPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(
request_certs_if_needed,
poll_request_certs_task,
handle_queued_certs_to_send,
)
.chain()
.before(chat::handler::handle_send_chat_kind_event),
);
}
}
#[derive(Component)]
pub struct RequestCertsTask(pub Task<Result<Certificates, FetchCertificatesError>>);
/// A component that makes us have to wait until the given time to refresh the
/// certs.
///
/// This is used to avoid spamming requests if requesting certs fails. Usually,
/// we just check [`Certificates::expires_at`].
#[derive(Component)]
pub struct OnlyRefreshCertsAfter {
pub refresh_at: Instant,
}
/// A component that's present when that this client has sent its certificates
/// to the server.
///
/// This should be removed if you want to re-send the certs.
///
/// If you want to get the client's actual certificates, you can get that from
/// the `certs` in the [`Account`] component.
#[derive(Component)]
pub struct ChatSigningSession {
pub session_id: Uuid,
pub messages_sent: u32,
}
pub fn poll_request_certs_task(
mut commands: Commands,
mut query: Query<(Entity, &mut RequestCertsTask, &Account)>,
) {
for (entity, mut auth_task, account) in query.iter_mut() {
if let Some(poll_res) = future::block_on(future::poll_once(&mut auth_task.0)) {
debug!("Finished requesting certs");
commands.entity(entity).remove::<RequestCertsTask>();
match poll_res {
Ok(certs) => {
commands.entity(entity).insert(QueuedCertsToSend {
certs: certs.clone(),
});
*account.certs.lock() = Some(certs);
}
Err(err) => {
error!("Error requesting certs: {err:?}. Retrying in an hour.");
commands.entity(entity).insert(OnlyRefreshCertsAfter {
refresh_at: Instant::now() + Duration::from_secs(60 * 60),
});
}
}
}
}
}
#[allow(clippy::type_complexity)]
pub fn request_certs_if_needed(
mut commands: Commands,
mut query: Query<
(
Entity,
&Account,
Option<&OnlyRefreshCertsAfter>,
Option<&ChatSigningSession>,
),
(
Without<RequestCertsTask>,
With<InGameState>,
With<IsAuthenticated>,
),
>,
) {
for (entity, account, only_refresh_certs_after, chat_signing_session) in query.iter_mut() {
if let Some(only_refresh_certs_after) = only_refresh_certs_after {
if only_refresh_certs_after.refresh_at > Instant::now() {
continue;
}
}
let certs = account.certs.lock();
let should_refresh = if let Some(certs) = &*certs {
// certs were already requested and we're waiting for them to refresh
// but maybe they weren't sent yet, in which case we still want to send the
// certs
if chat_signing_session.is_none() {
true
} else {
Utc::now() > certs.expires_at
}
} else {
true
};
drop(certs);
if should_refresh {
if let Some(access_token) = &account.access_token {
let task_pool = IoTaskPool::get();
let access_token = access_token.lock().clone();
debug!("Started task to fetch certs");
let task = task_pool.spawn(async_compat::Compat::new(async move {
azalea_auth::certs::fetch_certificates(&access_token).await
}));
commands
.entity(entity)
.insert(RequestCertsTask(task))
.remove::<OnlyRefreshCertsAfter>();
}
}
}
}
/// A component that's present on players that should send their chat signing
/// certificates as soon as possible.
///
/// This is removed when the certificates get sent.
#[derive(Component)]
pub struct QueuedCertsToSend {
pub certs: Certificates,
}
pub fn handle_queued_certs_to_send(
mut commands: Commands,
query: Query<(Entity, &QueuedCertsToSend), With<IsAuthenticated>>,
) {
for (entity, queued_certs) in &query {
let certs = &queued_certs.certs;
let session_id = Uuid::new_v4();
let chat_session = RemoteChatSessionData {
session_id,
profile_public_key: ProfilePublicKeyData {
expires_at: certs.expires_at.timestamp_millis() as u64,
key: certs.public_key_der.clone(),
key_signature: certs.signature_v2.clone(),
},
};
debug!("Sending chat signing certs to server");
commands.trigger(SendPacketEvent::new(
entity,
ServerboundChatSessionUpdate { chat_session },
));
commands
.entity(entity)
.remove::<QueuedCertsToSend>()
.insert(ChatSigningSession {
session_id,
messages_sent: 0,
});
}
}

View file

@ -17,7 +17,7 @@ use bevy_ecs::prelude::*;
use tracing::{error, trace};
use crate::{
InstanceHolder, interact::handle_block_interact_event, inventory::InventorySet,
InstanceHolder, interact::handle_start_use_item_queued, inventory::InventorySet,
packet::game::SendPacketEvent, respawn::perform_respawn,
};
@ -33,7 +33,7 @@ impl Plugin for ChunksPlugin {
)
.chain()
.before(InventorySet)
.before(handle_block_interact_event)
.before(handle_start_use_item_queued)
.before(perform_respawn),
)
.add_event::<ReceiveChunkEvent>()
@ -67,7 +67,7 @@ pub struct ChunkBatchFinishedEvent {
pub fn handle_receive_chunk_events(
mut events: EventReader<ReceiveChunkEvent>,
mut query: Query<&mut InstanceHolder>,
mut query: Query<&InstanceHolder>,
) {
for event in events.read() {
let pos = ChunkPos::new(event.packet.x, event.packet.z);

View file

@ -34,7 +34,6 @@ impl Plugin for ConnectionPlugin {
}
pub fn read_packets(ecs: &mut World) {
// receive_game_packet_events: EventWriter<ReceiveGamePacketEvent>,
let mut entity_and_conn_query = ecs.query::<(Entity, &mut RawConnection)>();
let mut conn_query = ecs.query::<&mut RawConnection>();

View file

@ -3,22 +3,12 @@
use azalea_chat::FormattedText;
use azalea_entity::{EntityBundle, InLoadedChunk, LocalEntity, metadata::PlayerMetadataBundle};
use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::{
component::Component,
entity::Entity,
event::{EventReader, EventWriter},
prelude::Event,
query::{Changed, With},
schedule::IntoSystemConfigs,
system::{Commands, Query},
};
use bevy_ecs::prelude::*;
use derive_more::Deref;
use tracing::trace;
use tracing::info;
use crate::{
InstanceHolder, client::JoinedClientBundle, connection::RawConnection,
events::LocalPlayerEvents,
};
use super::login::IsAuthenticated;
use crate::{InstanceHolder, chat_signing, client::JoinedClientBundle, connection::RawConnection};
pub struct DisconnectPlugin;
impl Plugin for DisconnectPlugin {
@ -27,21 +17,55 @@ impl Plugin for DisconnectPlugin {
PostUpdate,
(
update_read_packets_task_running_component,
disconnect_on_connection_dead,
remove_components_from_disconnected_players,
// this happens after `remove_components_from_disconnected_players` since that
// system removes `IsConnectionAlive`, which ensures that
// `DisconnectEvent` won't get called again from
// `disconnect_on_connection_dead`
disconnect_on_connection_dead,
)
.chain(),
);
}
}
/// An event sent when a client is getting disconnected.
/// An event sent when a client got disconnected from the server.
///
/// If the client was kicked with a reason, that reason will be present in the
/// [`reason`](DisconnectEvent::reason) field.
///
/// This event won't be sent if creating the initial connection to the server
/// failed, for that see [`ConnectionFailedEvent`].
///
/// [`ConnectionFailedEvent`]: crate::join::ConnectionFailedEvent
#[derive(Event)]
pub struct DisconnectEvent {
pub entity: Entity,
pub reason: Option<FormattedText>,
}
/// A bundle of components that are removed when a client disconnects.
///
/// This shouldn't be used for inserts because not all of the components should
/// always be present.
#[derive(Bundle)]
pub struct RemoveOnDisconnectBundle {
pub joined_client: JoinedClientBundle,
pub entity: EntityBundle,
pub instance_holder: InstanceHolder,
pub player_metadata: PlayerMetadataBundle,
pub in_loaded_chunk: InLoadedChunk,
//// This makes it close the TCP connection.
pub raw_connection: RawConnection,
/// This makes it not send [`DisconnectEvent`] again.
pub is_connection_alive: IsConnectionAlive,
/// Resend our chat signing certs next time.
pub chat_signing_session: chat_signing::ChatSigningSession,
/// They're not authenticated anymore if they disconnected.
pub is_authenticated: IsAuthenticated,
}
/// A system that removes the several components from our clients when they get
/// a [`DisconnectEvent`].
pub fn remove_components_from_disconnected_players(
@ -49,19 +73,18 @@ pub fn remove_components_from_disconnected_players(
mut events: EventReader<DisconnectEvent>,
mut loaded_by_query: Query<&mut azalea_entity::LoadedBy>,
) {
for DisconnectEvent { entity, .. } in events.read() {
trace!("Got DisconnectEvent for {entity:?}");
for DisconnectEvent { entity, reason } in events.read() {
info!(
"A client {entity:?} was disconnected{}",
if let Some(reason) = reason {
format!(": {reason}")
} else {
"".to_string()
}
);
commands
.entity(*entity)
.remove::<JoinedClientBundle>()
.remove::<EntityBundle>()
.remove::<InstanceHolder>()
.remove::<PlayerMetadataBundle>()
.remove::<InLoadedChunk>()
// this makes it close the tcp connection
.remove::<RawConnection>()
// swarm detects when this tx gets dropped to fire SwarmEvent::Disconnect
.remove::<LocalPlayerEvents>();
.remove::<RemoveOnDisconnectBundle>();
// note that we don't remove the client from the ECS, so if they decide
// to reconnect they'll keep their state
@ -94,7 +117,7 @@ fn disconnect_on_connection_dead(
) {
for (entity, &is_connection_alive) in &query {
if !*is_connection_alive {
disconnect_events.send(DisconnectEvent {
disconnect_events.write(DisconnectEvent {
entity,
reason: None,
});

View file

@ -1,5 +1,5 @@
//! Defines the [`Event`] enum and makes those events trigger when they're sent
//! in the ECS.
//! Defines the [`enum@Event`] enum and makes those events trigger when they're
//! sent in the ECS.
use std::sync::Arc;
@ -9,14 +9,7 @@ use azalea_entity::{Dead, InLoadedChunk};
use azalea_protocol::packets::game::c_player_combat_kill::ClientboundPlayerCombatKill;
use azalea_world::{InstanceName, MinecraftEntityId};
use bevy_app::{App, Plugin, PreUpdate, Update};
use bevy_ecs::{
component::Component,
entity::Entity,
event::EventReader,
query::{Added, With, Without},
schedule::IntoSystemConfigs,
system::{Commands, Query},
};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use tokio::sync::mpsc;

View file

@ -1,36 +1,30 @@
use std::ops::AddAssign;
use azalea_block::BlockState;
use azalea_core::{
block_hit_result::BlockHitResult,
direction::Direction,
game_type::GameMode,
hit_result::{BlockHitResult, HitResult},
position::{BlockPos, Vec3},
tick::GameTick,
};
use azalea_entity::{
Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector,
};
use azalea_inventory::{ItemStack, ItemStackData, components};
use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
use azalea_physics::{
PhysicsSet,
clip::{BlockShapeType, ClipContext, FluidPickType},
};
use azalea_protocol::packets::game::{
s_interact::InteractionHand,
s_swing::ServerboundSwing,
s_use_item_on::{BlockHit, ServerboundUseItemOn},
ServerboundUseItem, s_interact::InteractionHand, s_swing::ServerboundSwing,
s_use_item_on::ServerboundUseItemOn,
};
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::{
component::Component,
entity::Entity,
event::{Event, EventReader},
observer::Trigger,
query::{Changed, With},
schedule::IntoSystemConfigs,
system::{Commands, Query, Res},
};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use tracing::warn;
use super::mining::{Mining, MiningSet};
use crate::{
Client,
attack::handle_attack_event,
@ -45,14 +39,14 @@ use crate::{
pub struct InteractPlugin;
impl Plugin for InteractPlugin {
fn build(&self, app: &mut App) {
app.add_event::<BlockInteractEvent>()
app.add_event::<StartUseItemEvent>()
.add_event::<SwingArmEvent>()
.add_systems(
Update,
(
(
handle_start_use_item_event,
update_hit_result_component.after(clamp_look_direction),
handle_block_interact_event,
handle_swing_arm_event,
)
.after(InventorySet)
@ -64,34 +58,47 @@ impl Plugin for InteractPlugin {
.after(MoveEventsSet),
),
)
.add_systems(
GameTick,
handle_start_use_item_queued
.after(MiningSet)
.before(PhysicsSet),
)
.add_observer(handle_swing_arm_trigger);
}
}
impl Client {
/// Right click a block. The behavior of this depends on the target block,
/// Right-click a block.
///
/// The behavior of this depends on the target block,
/// and it'll either place the block you're holding in your hand or use the
/// block you clicked (like toggling a lever).
///
/// Note that this may trigger anticheats as it doesn't take into account
/// whether you're actually looking at the block.
pub fn block_interact(&self, position: BlockPos) {
self.ecs.lock().send_event(BlockInteractEvent {
self.ecs.lock().send_event(StartUseItemEvent {
entity: self.entity,
position,
hand: InteractionHand::MainHand,
force_block: Some(position),
});
}
}
/// Right click a block. The behavior of this depends on the target block,
/// and it'll either place the block you're holding in your hand or use the
/// block you clicked (like toggling a lever).
#[derive(Event)]
pub struct BlockInteractEvent {
/// The local player entity that's opening the container.
pub entity: Entity,
/// The coordinates of the container.
pub position: BlockPos,
/// Right-click the currently held item.
///
/// If the item is consumable, then it'll act as if right-click was held
/// until the item finishes being consumed. You can use this to eat food.
///
/// If we're looking at a block or entity, then it will be clicked. Also see
/// [`Client::block_interact`].
pub fn start_use_item(&self) {
self.ecs.lock().send_event(StartUseItemEvent {
entity: self.entity,
hand: InteractionHand::MainHand,
force_block: None,
});
}
}
/// A component that contains the number of changes this client has made to
@ -99,65 +106,150 @@ pub struct BlockInteractEvent {
#[derive(Component, Copy, Clone, Debug, Default, Deref)]
pub struct CurrentSequenceNumber(u32);
impl AddAssign<u32> for CurrentSequenceNumber {
fn add_assign(&mut self, rhs: u32) {
self.0 += rhs;
impl CurrentSequenceNumber {
/// Get the next sequence number that we're going to use and increment the
/// value.
pub fn get_and_increment(&mut self) -> u32 {
let cur = self.0;
self.0 += 1;
cur
}
}
/// A component that contains the block that the player is currently looking at.
/// A component that contains the block or entity that the player is currently
/// looking at.
#[doc(alias("looking at", "looking at block", "crosshair"))]
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct HitResultComponent(BlockHitResult);
pub struct HitResultComponent(HitResult);
pub fn handle_block_interact_event(
mut events: EventReader<BlockInteractEvent>,
mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>,
/// An event that makes one of our clients simulate a right-click.
///
/// This event just inserts the [`StartUseItemQueued`] component on the given
/// entity.
#[doc(alias("right click"))]
#[derive(Event)]
pub struct StartUseItemEvent {
pub entity: Entity,
pub hand: InteractionHand,
/// See [`QueuedStartUseItem::force_block`].
pub force_block: Option<BlockPos>,
}
pub fn handle_start_use_item_event(
mut commands: Commands,
mut events: EventReader<StartUseItemEvent>,
) {
for event in events.read() {
let Ok((entity, mut sequence_number, hit_result)) = query.get_mut(event.entity) else {
warn!("Sent BlockInteractEvent for entity that doesn't have the required components");
continue;
};
commands.entity(event.entity).insert(StartUseItemQueued {
hand: event.hand,
force_block: event.force_block,
});
}
}
// TODO: check to make sure we're within the world border
/// A component that makes our client simulate a right-click on the next
/// [`GameTick`]. It's removed after that tick.
///
/// You may find it more convenient to use [`StartUseItemEvent`] instead, which
/// just inserts this component for you.
///
/// [`GameTick`]: azalea_core::tick::GameTick
#[derive(Component)]
pub struct StartUseItemQueued {
pub hand: InteractionHand,
/// Optionally force us to send a [`ServerboundUseItemOn`] on the given
/// block.
///
/// This is useful if you want to interact with a block without looking at
/// it, but should be avoided to stay compatible with anticheats.
pub force_block: Option<BlockPos>,
}
#[allow(clippy::type_complexity)]
pub fn handle_start_use_item_queued(
mut commands: Commands,
query: Query<(
Entity,
&StartUseItemQueued,
&mut CurrentSequenceNumber,
&HitResultComponent,
&LookDirection,
Option<&Mining>,
)>,
) {
for (entity, start_use_item, mut sequence_number, hit_result, look_direction, mining) in query {
commands.entity(entity).remove::<StartUseItemQueued>();
*sequence_number += 1;
if mining.is_some() {
warn!("Got a StartUseItemEvent for a client that was mining");
}
// minecraft also does the interaction client-side (so it looks like clicking a
// button is instant) but we don't really need that
// TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
// rowing a boat
// the block_hit data will depend on whether we're looking at the block and
// whether we can reach it
let mut hit_result = hit_result.0.clone();
let block_hit = if hit_result.block_pos == event.position {
// we're looking at the block :)
BlockHit {
block_pos: hit_result.block_pos,
direction: hit_result.direction,
location: hit_result.location,
inside: hit_result.inside,
world_border: hit_result.world_border,
if let Some(force_block) = start_use_item.force_block {
let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
block_hit_result.block_pos == force_block
} else {
false
};
if !hit_result_matches {
// we're not looking at the block, so make up some numbers
hit_result = HitResult::Block(BlockHitResult {
location: force_block.center(),
direction: Direction::Up,
block_pos: force_block,
inside: false,
world_border: false,
miss: false,
});
}
} else {
// we're not looking at the block, so make up some numbers
BlockHit {
block_pos: event.position,
direction: Direction::Up,
location: event.position.center(),
inside: false,
world_border: false,
}
};
}
commands.trigger(SendPacketEvent::new(
entity,
ServerboundUseItemOn {
hand: InteractionHand::MainHand,
block_hit,
sequence: sequence_number.0,
},
));
match &hit_result {
HitResult::Block(block_hit_result) => {
if block_hit_result.miss {
commands.trigger(SendPacketEvent::new(
entity,
ServerboundUseItem {
hand: start_use_item.hand,
sequence: sequence_number.get_and_increment(),
x_rot: look_direction.x_rot,
y_rot: look_direction.y_rot,
},
));
} else {
commands.trigger(SendPacketEvent::new(
entity,
ServerboundUseItemOn {
hand: start_use_item.hand,
block_hit: block_hit_result.into(),
sequence: sequence_number.get_and_increment(),
},
));
// TODO: depending on the result of useItemOn, this might
// also need to send a SwingArmEvent.
// basically, this TODO is for
// simulating block interactions/placements on the
// client-side.
}
}
HitResult::Entity => {
// TODO: implement HitResult::Entity
// TODO: worldborder check
// commands.trigger(SendPacketEvent::new(
// entity,
// ServerboundInteract {
// entity_id: todo!(),
// action: todo!(),
// using_secondary_action: todo!(),
// },
// ));
}
}
}
}
@ -205,12 +297,32 @@ pub fn update_hit_result_component(
}
}
/// Get the block or entity that a player would be looking at if their eyes were
/// at the given direction and position.
///
/// If you need to get the block/entity the player is looking at right now, use
/// [`HitResultComponent`].
///
/// Also see [`pick_block`].
///
/// TODO: does not currently check for entities
pub fn pick(
look_direction: &LookDirection,
eye_position: &Vec3,
chunks: &azalea_world::ChunkStorage,
pick_range: f64,
) -> HitResult {
// TODO
// let entity_hit_result = ;
HitResult::Block(pick_block(look_direction, eye_position, chunks, pick_range))
}
/// Get the block that a player would be looking at if their eyes were at the
/// given direction and position.
///
/// If you need to get the block the player is looking at right now, use
/// [`HitResultComponent`].
pub fn pick(
/// Also see [`pick`].
pub fn pick_block(
look_direction: &LookDirection,
eye_position: &Vec3,
chunks: &azalea_world::ChunkStorage,
@ -218,6 +330,7 @@ pub fn pick(
) -> BlockHitResult {
let view_vector = view_vector(look_direction);
let end_position = eye_position + &(view_vector * pick_range);
azalea_physics::clip::clip(
chunks,
ClipContext {

View file

@ -16,14 +16,7 @@ use azalea_protocol::packets::game::{
};
use azalea_registry::MenuKind;
use bevy_app::{App, Plugin, Update};
use bevy_ecs::{
component::Component,
entity::Entity,
event::EventReader,
prelude::{Event, EventWriter},
schedule::{IntoSystemConfigs, SystemSet},
system::{Commands, Query},
};
use bevy_ecs::prelude::*;
use tracing::{error, warn};
use crate::{
@ -628,7 +621,7 @@ fn handle_container_close_event(
container_id: inventory.id,
},
));
client_side_events.send(ClientSideCloseContainerEvent {
client_side_events.write(ClientSideCloseContainerEvent {
entity: event.entity,
});
}

View file

@ -0,0 +1,266 @@
use std::{io, net::SocketAddr, sync::Arc};
use azalea_entity::{LocalEntity, indexing::EntityUuidIndex};
use azalea_protocol::{
ServerAddress,
connect::{Connection, ConnectionError, Proxy},
packets::{
ClientIntention, ConnectionProtocol, PROTOCOL_VERSION,
handshake::ServerboundIntention,
login::{ClientboundLoginPacket, ServerboundHello, ServerboundLoginPacket},
},
};
use azalea_world::Instance;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_tasks::{IoTaskPool, Task, futures_lite::future};
use parking_lot::RwLock;
use tokio::sync::mpsc;
use tracing::{debug, warn};
use super::events::LocalPlayerEvents;
use crate::{
Account, JoinError, LocalPlayerBundle,
connection::RawConnection,
packet::login::{InLoginState, SendLoginPacketEvent},
};
/// A plugin that allows bots to join servers.
pub struct JoinPlugin;
impl Plugin for JoinPlugin {
fn build(&self, app: &mut App) {
app.add_event::<StartJoinServerEvent>()
.add_event::<ConnectionFailedEvent>()
.add_systems(
Update,
(
handle_start_join_server_event.before(super::login::poll_auth_task),
poll_create_connection_task,
handle_connection_failed_events,
)
.chain(),
);
}
}
/// An event to make a client join the server and be added to our swarm.
///
/// This won't do anything if a client with the Account UUID is already
/// connected to the server.
#[derive(Event, Debug)]
pub struct StartJoinServerEvent {
pub account: Account,
pub connect_opts: ConnectOpts,
pub event_sender: Option<mpsc::UnboundedSender<crate::Event>>,
pub start_join_callback_tx: Option<StartJoinCallback>,
}
/// Options for how the connection to the server will be made. These are
/// persisted on reconnects.
///
/// This is inserted as a component on clients to make auto-reconnecting work.
#[derive(Debug, Clone, Component)]
pub struct ConnectOpts {
pub address: ServerAddress,
pub resolved_address: SocketAddr,
pub proxy: Option<Proxy>,
}
/// An event that's sent when creating the TCP connection and sending the first
/// packet fails.
///
/// This isn't sent if we're kicked later, see [`DisconnectEvent`].
///
/// [`DisconnectEvent`]: crate::disconnect::DisconnectEvent
#[derive(Event)]
pub struct ConnectionFailedEvent {
pub entity: Entity,
pub error: ConnectionError,
}
// this is mpsc instead of oneshot so it can be cloned (since it's sent in an
// event)
#[derive(Component, Debug, Clone)]
pub struct StartJoinCallback(pub mpsc::UnboundedSender<Result<Entity, JoinError>>);
pub fn handle_start_join_server_event(
mut commands: Commands,
mut events: EventReader<StartJoinServerEvent>,
mut entity_uuid_index: ResMut<EntityUuidIndex>,
connection_query: Query<&RawConnection>,
) {
for event in events.read() {
let uuid = event.account.uuid_or_offline();
let entity = if let Some(entity) = entity_uuid_index.get(&uuid) {
debug!("Reusing entity {entity:?} for client");
// check if it's already connected
if let Ok(conn) = connection_query.get(entity) {
if conn.is_alive() {
if let Some(start_join_callback_tx) = &event.start_join_callback_tx {
warn!(
"Received StartJoinServerEvent for {entity:?} but it's already connected. Ignoring the event but replying with Ok."
);
let _ = start_join_callback_tx.0.send(Ok(entity));
} else {
warn!(
"Received StartJoinServerEvent for {entity:?} but it's already connected. Ignoring the event."
);
}
return;
}
}
entity
} else {
let entity = commands.spawn_empty().id();
debug!("Created new entity {entity:?} for client");
// add to the uuid index
entity_uuid_index.insert(uuid, entity);
entity
};
let mut entity_mut = commands.entity(entity);
entity_mut.insert((
// add the Account to the entity now so plugins can access it earlier
event.account.to_owned(),
// localentity is always present for our clients, even if we're not actually logged
// in
LocalEntity,
// ConnectOpts is inserted as a component here
event.connect_opts.clone(),
// we don't insert InLoginState until we actually create the connection. note that
// there's no InHandshakeState component since we switch off of the handshake state
// immediately when the connection is created
));
if let Some(event_sender) = &event.event_sender {
// this is optional so we don't leak memory in case the user doesn't want to
// handle receiving packets
entity_mut.insert(LocalPlayerEvents(event_sender.clone()));
}
if let Some(start_join_callback) = &event.start_join_callback_tx {
entity_mut.insert(start_join_callback.clone());
}
let task_pool = IoTaskPool::get();
let connect_opts = event.connect_opts.clone();
let task = task_pool.spawn(async_compat::Compat::new(
create_conn_and_send_intention_packet(connect_opts),
));
entity_mut.insert(CreateConnectionTask(task));
}
}
async fn create_conn_and_send_intention_packet(
opts: ConnectOpts,
) -> Result<LoginConn, ConnectionError> {
let mut conn = if let Some(proxy) = opts.proxy {
Connection::new_with_proxy(&opts.resolved_address, proxy).await?
} else {
Connection::new(&opts.resolved_address).await?
};
conn.write(ServerboundIntention {
protocol_version: PROTOCOL_VERSION,
hostname: opts.address.host.clone(),
port: opts.address.port,
intention: ClientIntention::Login,
})
.await?;
let conn = conn.login();
Ok(conn)
}
type LoginConn = Connection<ClientboundLoginPacket, ServerboundLoginPacket>;
#[derive(Component)]
pub struct CreateConnectionTask(pub Task<Result<LoginConn, ConnectionError>>);
pub fn poll_create_connection_task(
mut commands: Commands,
mut query: Query<(
Entity,
&mut CreateConnectionTask,
&Account,
Option<&StartJoinCallback>,
)>,
mut connection_failed_events: EventWriter<ConnectionFailedEvent>,
) {
for (entity, mut task, account, mut start_join_callback) in query.iter_mut() {
if let Some(poll_res) = future::block_on(future::poll_once(&mut task.0)) {
let mut entity_mut = commands.entity(entity);
entity_mut.remove::<CreateConnectionTask>();
let conn = match poll_res {
Ok(conn) => conn,
Err(error) => {
warn!("failed to create connection: {error}");
connection_failed_events.write(ConnectionFailedEvent { entity, error });
return;
}
};
let (read_conn, write_conn) = conn.into_split();
let (read_conn, write_conn) = (read_conn.raw, write_conn.raw);
let instance = Instance::default();
let instance_holder = crate::local_player::InstanceHolder::new(
entity,
// default to an empty world, it'll be set correctly later when we
// get the login packet
Arc::new(RwLock::new(instance)),
);
entity_mut.insert((
// these stay when we switch to the game state
LocalPlayerBundle {
raw_connection: RawConnection::new(
read_conn,
write_conn,
ConnectionProtocol::Login,
),
client_information: crate::ClientInformation::default(),
instance_holder,
metadata: azalea_entity::metadata::PlayerMetadataBundle::default(),
},
InLoginState,
));
commands.trigger(SendLoginPacketEvent::new(
entity,
ServerboundHello {
name: account.username.clone(),
profile_id: account.uuid_or_offline(),
},
));
if let Some(cb) = start_join_callback.take() {
let _ = cb.0.send(Ok(entity));
}
}
}
}
pub fn handle_connection_failed_events(
mut events: EventReader<ConnectionFailedEvent>,
query: Query<&StartJoinCallback>,
) {
for event in events.read() {
let Ok(start_join_callback) = query.get(event.entity) else {
// the StartJoinCallback isn't required to be present, so this is fine
continue;
};
// io::Error isn't clonable, so we create a new one based on the `kind` and
// `to_string`,
let ConnectionError::Io(err) = &event.error;
let cloned_err = ConnectionError::Io(io::Error::new(err.kind(), err.to_string()));
let _ = start_join_callback.0.send(Err(cloned_err.into()));
}
}

View file

@ -27,20 +27,28 @@ fn handle_receive_hello_event(trigger: Trigger<ReceiveHelloEvent>, mut commands:
let account = trigger.account.clone();
let packet = trigger.packet.clone();
let player = trigger.entity();
let player = trigger.target();
let task = task_pool.spawn(auth_with_account(account, packet));
commands.entity(player).insert(AuthTask(task));
}
fn poll_auth_task(
/// A marker component on our clients that indicates that the server is
/// online-mode and the client has authenticated their join with Mojang.
#[derive(Component)]
pub struct IsAuthenticated;
pub fn poll_auth_task(
mut commands: Commands,
mut query: Query<(Entity, &mut AuthTask, &mut RawConnection)>,
) {
for (entity, mut auth_task, mut raw_conn) in query.iter_mut() {
if let Some(poll_res) = future::block_on(future::poll_once(&mut auth_task.0)) {
debug!("Finished auth");
commands.entity(entity).remove::<AuthTask>();
commands
.entity(entity)
.remove::<AuthTask>()
.insert(IsAuthenticated);
match poll_res {
Ok((packet, private_key)) => {
// we use this instead of SendLoginPacketEvent to ensure that it's sent right

View file

@ -2,7 +2,7 @@ use azalea_block::{Block, BlockState, fluid_state::FluidState};
use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
use azalea_entity::{FluidOnEyes, Physics, mining::get_mine_progress};
use azalea_inventory::ItemStack;
use azalea_physics::PhysicsSet;
use azalea_physics::{PhysicsSet, collision::BlockWithShape};
use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction};
use azalea_world::{InstanceContainer, InstanceName};
use bevy_app::{App, Plugin, Update};
@ -10,7 +10,7 @@ use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use crate::{
Client,
Client, InstanceHolder,
interact::{
CurrentSequenceNumber, HitResultComponent, SwingArmEvent, can_use_game_master_blocks,
check_is_interaction_restricted,
@ -26,22 +26,26 @@ pub struct MiningPlugin;
impl Plugin for MiningPlugin {
fn build(&self, app: &mut App) {
app.add_event::<StartMiningBlockEvent>()
.add_event::<StartMiningBlockWithDirectionEvent>()
.add_event::<FinishMiningBlockEvent>()
.add_event::<StopMiningBlockEvent>()
.add_event::<MineBlockProgressEvent>()
.add_event::<AttackBlockEvent>()
.add_systems(
GameTick,
(continue_mining_block, handle_auto_mine)
(
update_mining_component,
continue_mining_block,
handle_auto_mine,
handle_mining_queued,
)
.chain()
.before(PhysicsSet),
.before(PhysicsSet)
.in_set(MiningSet),
)
.add_systems(
Update,
(
handle_start_mining_block_event,
handle_start_mining_block_with_direction_event,
handle_finish_mining_block_event,
handle_stop_mining_block_event,
)
@ -53,7 +57,7 @@ impl Plugin for MiningPlugin {
.after(azalea_entity::update_fluid_on_eyes)
.after(crate::interact::update_hit_result_component)
.after(crate::attack::handle_attack_event)
.after(crate::interact::handle_block_interact_event)
.after(crate::interact::handle_start_use_item_queued)
.before(crate::interact::handle_swing_arm_event),
);
}
@ -65,7 +69,9 @@ pub struct MiningSet;
impl Client {
pub fn start_mining(&self, position: BlockPos) {
self.ecs.lock().send_event(StartMiningBlockEvent {
let mut ecs = self.ecs.lock();
ecs.send_event(StartMiningBlockEvent {
entity: self.entity,
position,
});
@ -116,23 +122,26 @@ fn handle_auto_mine(
current_mining_item,
) in &mut query.iter_mut()
{
let block_pos = hit_result_component.block_pos;
let block_pos = hit_result_component
.as_block_hit_result_if_not_miss()
.map(|b| b.block_pos);
if (mining.is_none()
|| !is_same_mining_target(
block_pos,
inventory,
current_mining_pos,
current_mining_item,
))
&& !hit_result_component.miss
// start mining if we're looking at a block and we're not already mining it
if let Some(block_pos) = block_pos
&& (mining.is_none()
|| !is_same_mining_target(
block_pos,
inventory,
current_mining_pos,
current_mining_item,
))
{
start_mining_block_event.send(StartMiningBlockEvent {
start_mining_block_event.write(StartMiningBlockEvent {
entity,
position: block_pos,
});
} else if mining.is_some() && hit_result_component.miss {
stop_mining_block_event.send(StopMiningBlockEvent { entity });
} else if mining.is_some() && hit_result_component.is_miss() {
stop_mining_block_event.write(StopMiningBlockEvent { entity });
}
}
}
@ -155,42 +164,44 @@ pub struct StartMiningBlockEvent {
pub position: BlockPos,
}
fn handle_start_mining_block_event(
mut commands: Commands,
mut events: EventReader<StartMiningBlockEvent>,
mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
mut query: Query<&HitResultComponent>,
) {
for event in events.read() {
let hit_result = query.get_mut(event.entity).unwrap();
let direction = if hit_result.block_pos == event.position {
let direction = if let Some(block_hit_result) = hit_result.as_block_hit_result_if_not_miss()
&& block_hit_result.block_pos == event.position
{
// we're looking at the block
hit_result.direction
block_hit_result.direction
} else {
// we're not looking at the block, arbitrary direction
Direction::Down
};
start_mining_events.send(StartMiningBlockWithDirectionEvent {
entity: event.entity,
commands.entity(event.entity).insert(MiningQueued {
position: event.position,
direction,
});
}
}
#[derive(Event)]
pub struct StartMiningBlockWithDirectionEvent {
pub entity: Entity,
/// Present on entities when they're going to start mining a block next tick.
#[derive(Component)]
pub struct MiningQueued {
pub position: BlockPos,
pub direction: Direction,
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
fn handle_start_mining_block_with_direction_event(
mut events: EventReader<StartMiningBlockWithDirectionEvent>,
mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
fn handle_mining_queued(
mut commands: Commands,
mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
mut attack_block_events: EventWriter<AttackBlockEvent>,
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
mut query: Query<(
&InstanceName,
query: Query<(
Entity,
&MiningQueued,
&InstanceHolder,
&LocalGameMode,
&Inventory,
&FluidOnEyes,
@ -203,29 +214,30 @@ fn handle_start_mining_block_with_direction_event(
&mut MineItem,
&mut MineBlockPos,
)>,
instances: Res<InstanceContainer>,
) {
for event in events.read() {
let (
instance_name,
game_mode,
inventory,
fluid_on_eyes,
physics,
mining,
mut sequence_number,
mut mine_delay,
mut mine_progress,
mut mine_ticks,
mut current_mining_item,
mut current_mining_pos,
) = query.get_mut(event.entity).unwrap();
for (
entity,
mining_queued,
instance_holder,
game_mode,
inventory,
fluid_on_eyes,
physics,
mining,
mut sequence_number,
mut mine_delay,
mut mine_progress,
mut mine_ticks,
mut current_mining_item,
mut current_mining_pos,
) in query
{
commands.entity(entity).remove::<MiningQueued>();
let instance_lock = instances.get(instance_name).unwrap();
let instance = instance_lock.read();
let instance = instance_holder.instance.read();
if check_is_interaction_restricted(
&instance,
&event.position,
&mining_queued.position,
&game_mode.current,
inventory,
) {
@ -235,15 +247,14 @@ fn handle_start_mining_block_with_direction_event(
// is outside of the worldborder
if game_mode.current == GameMode::Creative {
*sequence_number += 1;
finish_mining_events.send(FinishMiningBlockEvent {
entity: event.entity,
position: event.position,
finish_mining_events.write(FinishMiningBlockEvent {
entity,
position: mining_queued.position,
});
**mine_delay = 5;
} else if mining.is_none()
|| !is_same_mining_target(
event.position,
mining_queued.position,
inventory,
&current_mining_pos,
&current_mining_item,
@ -252,40 +263,29 @@ fn handle_start_mining_block_with_direction_event(
if mining.is_some() {
// send a packet to stop mining since we just changed target
commands.trigger(SendPacketEvent::new(
event.entity,
entity,
ServerboundPlayerAction {
action: s_player_action::Action::AbortDestroyBlock,
pos: current_mining_pos
.expect("IsMining is true so MineBlockPos must be present"),
direction: event.direction,
direction: mining_queued.direction,
sequence: 0,
},
));
}
let target_block_state = instance
.get_block_state(&event.position)
.get_block_state(&mining_queued.position)
.unwrap_or_default();
*sequence_number += 1;
let target_registry_block = azalea_registry::Block::from(target_block_state);
// we can't break blocks if they don't have a bounding box
// TODO: So right now azalea doesn't differenciate between different types of
// bounding boxes. See ClipContext::block_shape for more info. Ideally this
// should just call ClipContext::block_shape and check if it's empty.
let block_is_solid = !target_block_state.is_air()
// this is a hack to make sure we can't break water or lava
&& !matches!(
target_registry_block,
azalea_registry::Block::Water | azalea_registry::Block::Lava
);
let block_is_solid = !target_block_state.outline_shape().is_empty();
if block_is_solid && **mine_progress == 0. {
// interact with the block (like note block left click) here
attack_block_events.send(AttackBlockEvent {
entity: event.entity,
position: event.position,
attack_block_events.write(AttackBlockEvent {
entity,
position: mining_queued.position,
});
}
@ -303,35 +303,37 @@ fn handle_start_mining_block_with_direction_event(
) >= 1.
{
// block was broken instantly
finish_mining_events.send(FinishMiningBlockEvent {
entity: event.entity,
position: event.position,
finish_mining_events.write(FinishMiningBlockEvent {
entity,
position: mining_queued.position,
});
} else {
commands.entity(event.entity).insert(Mining {
pos: event.position,
dir: event.direction,
commands.entity(entity).insert(Mining {
pos: mining_queued.position,
dir: mining_queued.direction,
});
**current_mining_pos = Some(event.position);
**current_mining_pos = Some(mining_queued.position);
**current_mining_item = held_item;
**mine_progress = 0.;
**mine_ticks = 0.;
mine_block_progress_events.send(MineBlockProgressEvent {
entity: event.entity,
position: event.position,
mine_block_progress_events.write(MineBlockProgressEvent {
entity,
position: mining_queued.position,
destroy_stage: mine_progress.destroy_stage(),
});
}
commands.trigger(SendPacketEvent::new(
event.entity,
entity,
ServerboundPlayerAction {
action: s_player_action::Action::StartDestroyBlock,
pos: event.position,
direction: event.direction,
sequence: **sequence_number,
pos: mining_queued.position,
direction: mining_queued.direction,
sequence: sequence_number.get_and_increment(),
},
));
commands.trigger(SwingArmEvent { entity });
commands.trigger(SwingArmEvent { entity });
}
}
}
@ -502,7 +504,7 @@ pub fn handle_stop_mining_block_event(
));
commands.entity(event.entity).remove::<Mining>();
**mine_progress = 0.;
mine_block_progress_events.send(MineBlockProgressEvent {
mine_block_progress_events.write(MineBlockProgressEvent {
entity: event.entity,
position: mine_block_pos,
destroy_stage: None,
@ -530,8 +532,6 @@ pub fn continue_mining_block(
mut commands: Commands,
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
mut swing_arm_events: EventWriter<SwingArmEvent>,
instances: Res<InstanceContainer>,
) {
for (
@ -558,31 +558,33 @@ pub fn continue_mining_block(
if game_mode.current == GameMode::Creative {
// TODO: worldborder check
**mine_delay = 5;
finish_mining_events.send(FinishMiningBlockEvent {
finish_mining_events.write(FinishMiningBlockEvent {
entity,
position: mining.pos,
});
*sequence_number += 1;
commands.trigger(SendPacketEvent::new(
entity,
ServerboundPlayerAction {
action: s_player_action::Action::StartDestroyBlock,
pos: mining.pos,
direction: mining.dir,
sequence: **sequence_number,
sequence: sequence_number.get_and_increment(),
},
));
swing_arm_events.send(SwingArmEvent { entity });
commands.trigger(SwingArmEvent { entity });
} else if is_same_mining_target(
mining.pos,
inventory,
current_mining_pos,
current_mining_item,
) {
println!("continue mining block at {:?}", mining.pos);
let instance_lock = instances.get(instance_name).unwrap();
let instance = instance_lock.read();
let target_block_state = instance.get_block_state(&mining.pos).unwrap_or_default();
println!("target_block_state: {target_block_state:?}");
if target_block_state.is_air() {
commands.entity(entity).remove::<Mining>();
continue;
@ -603,8 +605,8 @@ pub fn continue_mining_block(
if **mine_progress >= 1. {
commands.entity(entity).remove::<Mining>();
*sequence_number += 1;
finish_mining_events.send(FinishMiningBlockEvent {
println!("finished mining block at {:?}", mining.pos);
finish_mining_events.write(FinishMiningBlockEvent {
entity,
position: mining.pos,
});
@ -614,7 +616,7 @@ pub fn continue_mining_block(
action: s_player_action::Action::StopDestroyBlock,
pos: mining.pos,
direction: mining.dir,
sequence: **sequence_number,
sequence: sequence_number.get_and_increment(),
},
));
**mine_progress = 0.;
@ -622,20 +624,32 @@ pub fn continue_mining_block(
**mine_delay = 0;
}
mine_block_progress_events.send(MineBlockProgressEvent {
mine_block_progress_events.write(MineBlockProgressEvent {
entity,
position: mining.pos,
destroy_stage: mine_progress.destroy_stage(),
});
swing_arm_events.send(SwingArmEvent { entity });
commands.trigger(SwingArmEvent { entity });
} else {
start_mining_events.send(StartMiningBlockWithDirectionEvent {
entity,
println!("switching mining target to {:?}", mining.pos);
commands.entity(entity).insert(MiningQueued {
position: mining.pos,
direction: mining.dir,
});
}
swing_arm_events.send(SwingArmEvent { entity });
}
}
pub fn update_mining_component(
mut commands: Commands,
mut query: Query<(Entity, &mut Mining, &HitResultComponent)>,
) {
for (entity, mut mining, hit_result_component) in &mut query.iter_mut() {
if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() {
mining.pos = block_hit_result.block_pos;
mining.dir = block_hit_result.direction;
} else {
commands.entity(entity).remove::<Mining>();
}
}
}

View file

@ -1,14 +1,17 @@
use bevy_app::{PluginGroup, PluginGroupBuilder};
pub mod attack;
pub mod auto_reconnect;
pub mod brand;
pub mod chat;
pub mod chat_signing;
pub mod chunks;
pub mod connection;
pub mod disconnect;
pub mod events;
pub mod interact;
pub mod inventory;
pub mod join;
pub mod login;
pub mod mining;
pub mod movement;
@ -49,7 +52,10 @@ impl PluginGroup for DefaultPlugins {
.add(tick_broadcast::TickBroadcastPlugin)
.add(pong::PongPlugin)
.add(connection::ConnectionPlugin)
.add(login::LoginPlugin);
.add(login::LoginPlugin)
.add(join::JoinPlugin)
.add(auto_reconnect::AutoReconnectPlugin)
.add(chat_signing::ChatSigningPlugin);
#[cfg(feature = "log")]
{
group = group.add(bevy_log::LogPlugin::default());

View file

@ -17,13 +17,7 @@ use azalea_protocol::packets::{
};
use azalea_world::{MinecraftEntityId, MoveEntityError};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::Event;
use bevy_ecs::schedule::SystemSet;
use bevy_ecs::system::Commands;
use bevy_ecs::{
component::Component, entity::Entity, event::EventReader, query::With,
schedule::IntoSystemConfigs, system::Query,
};
use bevy_ecs::prelude::*;
use thiserror::Error;
use crate::client::Client;

View file

@ -64,7 +64,7 @@ pub struct ConfigPacketHandler<'a> {
}
impl ConfigPacketHandler<'_> {
pub fn registry_data(&mut self, p: &ClientboundRegistryData) {
as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
as_system::<Query<&InstanceHolder>>(self.ecs, |mut query| {
let instance_holder = query.get_mut(self.player).unwrap();
let mut instance = instance_holder.instance.write();
@ -82,7 +82,7 @@ impl ConfigPacketHandler<'_> {
pub fn disconnect(&mut self, p: &ClientboundDisconnect) {
warn!("Got disconnect packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(DisconnectEvent {
events.write(DisconnectEvent {
entity: self.player,
reason: Some(p.reason.clone()),
});
@ -96,12 +96,12 @@ impl ConfigPacketHandler<'_> {
self.ecs,
|(mut commands, mut query)| {
let mut raw_conn = query.get_mut(self.player).unwrap();
raw_conn.state = ConnectionProtocol::Game;
commands.trigger(SendConfigPacketEvent::new(
self.player,
ServerboundFinishConfiguration,
));
raw_conn.state = ConnectionProtocol::Game;
// these components are added now that we're going to be in the Game state
commands
@ -124,7 +124,7 @@ impl ConfigPacketHandler<'_> {
);
as_system::<(Commands, EventWriter<_>)>(self.ecs, |(mut commands, mut events)| {
events.send(KeepAliveEvent {
events.write(KeepAliveEvent {
entity: self.player,
id: p.id,
});
@ -147,7 +147,7 @@ impl ConfigPacketHandler<'_> {
debug!("Got resource pack push packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(ResourcePackEvent {
events.write(ResourcePackEvent {
entity: self.player,
id: p.id,
url: p.url.to_owned(),

View file

@ -9,7 +9,7 @@ use azalea_protocol::packets::{
use azalea_world::Instance;
use bevy_ecs::prelude::*;
use parking_lot::RwLock;
use tracing::error;
use tracing::{error, trace};
use uuid::Uuid;
use crate::{PlayerInfo, client::InGameState, connection::RawConnection};
@ -60,6 +60,7 @@ pub fn handle_outgoing_packets_observer(
mut query: Query<(&mut RawConnection, Option<&InGameState>)>,
) {
let event = trigger.event();
trace!("Sending game packet: {:?}", event.packet);
if let Ok((mut raw_connection, in_game_state)) = query.get_mut(event.sent_by) {
if in_game_state.is_none() {

View file

@ -13,7 +13,7 @@ use azalea_entity::{
indexing::{EntityIdIndex, EntityUuidIndex},
metadata::{Health, apply_metadata},
};
use azalea_protocol::packets::game::*;
use azalea_protocol::packets::{ConnectionProtocol, game::*};
use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
use bevy_ecs::{prelude::*, system::SystemState};
pub use events::*;
@ -22,7 +22,9 @@ use tracing::{debug, error, trace, warn};
use crate::{
ClientInformation, PlayerInfo,
chat::{ChatPacket, ChatReceivedEvent},
chunks, declare_packet_handlers,
chunks,
connection::RawConnection,
declare_packet_handlers,
disconnect::DisconnectEvent,
inventory::{
ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent,
@ -226,7 +228,7 @@ impl GamePacketHandler<'_> {
let new_instance_name = p.common.dimension.clone();
if let Some(mut instance_name) = instance_name {
*instance_name = instance_name.clone();
**instance_name = new_instance_name.clone();
} else {
commands
.entity(self.player)
@ -242,13 +244,13 @@ impl GamePacketHandler<'_> {
// add this world to the instance_container (or don't if it's already
// there)
let weak_instance = instance_container.insert(
let weak_instance = instance_container.get_or_insert(
new_instance_name.clone(),
dimension_data.height,
dimension_data.min_y,
&instance_holder.instance.read().registries,
);
instance_loaded_events.send(InstanceLoadedEvent {
instance_loaded_events.write(InstanceLoadedEvent {
entity: self.player,
name: new_instance_name.clone(),
instance: Arc::downgrade(&weak_instance),
@ -338,7 +340,7 @@ impl GamePacketHandler<'_> {
debug!("Got chunk batch start");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(chunks::ChunkBatchStartEvent {
events.write(chunks::ChunkBatchStartEvent {
entity: self.player,
});
});
@ -348,7 +350,7 @@ impl GamePacketHandler<'_> {
debug!("Got chunk batch finished {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(chunks::ChunkBatchFinishedEvent {
events.write(chunks::ChunkBatchFinishedEvent {
entity: self.player,
batch_size: p.batch_size,
});
@ -389,7 +391,7 @@ impl GamePacketHandler<'_> {
warn!("Got disconnect packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(DisconnectEvent {
events.write(DisconnectEvent {
entity: self.player,
reason: Some(p.reason.clone()),
});
@ -531,7 +533,7 @@ impl GamePacketHandler<'_> {
display_name: updated_info.display_name.clone(),
};
tab_list.insert(updated_info.profile.uuid, info.clone());
add_player_events.send(AddPlayerEvent {
add_player_events.write(AddPlayerEvent {
entity: self.player,
info,
});
@ -547,7 +549,7 @@ impl GamePacketHandler<'_> {
if p.actions.update_display_name {
info.display_name.clone_from(&updated_info.display_name);
}
update_player_events.send(UpdatePlayerEvent {
update_player_events.write(UpdatePlayerEvent {
entity: self.player,
info: info.clone(),
});
@ -576,7 +578,7 @@ impl GamePacketHandler<'_> {
for uuid in &p.profile_ids {
if let Some(info) = tab_list.remove(uuid) {
remove_player_events.send(RemovePlayerEvent {
remove_player_events.write(RemovePlayerEvent {
entity: self.player,
info,
});
@ -590,7 +592,7 @@ impl GamePacketHandler<'_> {
pub fn set_chunk_cache_center(&mut self, p: &ClientboundSetChunkCacheCenter) {
debug!("Got chunk cache center packet {p:?}");
as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
as_system::<Query<&InstanceHolder>>(self.ecs, |mut query| {
let instance_holder = query.get_mut(self.player).unwrap();
let mut partial_world = instance_holder.partial_instance.write();
@ -610,7 +612,7 @@ impl GamePacketHandler<'_> {
debug!("Got chunk with light packet {} {}", p.x, p.z);
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(chunks::ReceiveChunkEvent {
events.write(chunks::ReceiveChunkEvent {
entity: self.player,
packet: p.clone(),
});
@ -1031,7 +1033,7 @@ impl GamePacketHandler<'_> {
as_system::<(EventWriter<KeepAliveEvent>, Commands)>(
self.ecs,
|(mut keepalive_events, mut commands)| {
keepalive_events.send(KeepAliveEvent {
keepalive_events.write(KeepAliveEvent {
entity: self.player,
id: p.id,
});
@ -1083,7 +1085,7 @@ impl GamePacketHandler<'_> {
debug!("Got player chat packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(ChatReceivedEvent {
events.write(ChatReceivedEvent {
entity: self.player,
packet: ChatPacket::Player(Arc::new(p.clone())),
});
@ -1094,7 +1096,7 @@ impl GamePacketHandler<'_> {
debug!("Got system chat packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(ChatReceivedEvent {
events.write(ChatReceivedEvent {
entity: self.player,
packet: ChatPacket::System(Arc::new(p.clone())),
});
@ -1105,7 +1107,7 @@ impl GamePacketHandler<'_> {
debug!("Got disguised chat packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(ChatReceivedEvent {
events.write(ChatReceivedEvent {
entity: self.player,
packet: ChatPacket::Disguised(Arc::new(p.clone())),
});
@ -1121,7 +1123,7 @@ impl GamePacketHandler<'_> {
pub fn block_update(&mut self, p: &ClientboundBlockUpdate) {
debug!("Got block update packet {p:?}");
as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
as_system::<Query<&InstanceHolder>>(self.ecs, |mut query| {
let local_player = query.get_mut(self.player).unwrap();
let world = local_player.instance.write();
@ -1137,7 +1139,7 @@ impl GamePacketHandler<'_> {
pub fn section_blocks_update(&mut self, p: &ClientboundSectionBlocksUpdate) {
debug!("Got section blocks update packet {p:?}");
as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
as_system::<Query<&InstanceHolder>>(self.ecs, |mut query| {
let local_player = query.get_mut(self.player).unwrap();
let world = local_player.instance.write();
for state in &p.states {
@ -1216,7 +1218,7 @@ impl GamePacketHandler<'_> {
}
}
} else {
events.send(SetContainerContentEvent {
events.write(SetContainerContentEvent {
entity: self.player,
slots: p.items.clone(),
container_id: p.container_id,
@ -1282,7 +1284,7 @@ impl GamePacketHandler<'_> {
debug!("Got container close packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(ClientSideCloseContainerEvent {
events.write(ClientSideCloseContainerEvent {
entity: self.player,
});
});
@ -1299,7 +1301,7 @@ impl GamePacketHandler<'_> {
as_system::<EventWriter<_>>(self.ecs, |mut knockback_events| {
if let Some(knockback) = p.knockback {
knockback_events.send(KnockbackEvent {
knockback_events.write(KnockbackEvent {
entity: self.player,
knockback: KnockbackType::Set(knockback),
});
@ -1310,7 +1312,7 @@ impl GamePacketHandler<'_> {
pub fn forget_level_chunk(&mut self, p: &ClientboundForgetLevelChunk) {
debug!("Got forget level chunk packet {p:?}");
as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
as_system::<Query<&InstanceHolder>>(self.ecs, |mut query| {
let local_player = query.get_mut(self.player).unwrap();
let mut partial_instance = local_player.partial_instance.write();
@ -1333,7 +1335,7 @@ impl GamePacketHandler<'_> {
debug!("Got open screen packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(MenuOpenedEvent {
events.write(MenuOpenedEvent {
entity: self.player,
window_id: p.container_id,
menu_type: p.menu_type,
@ -1370,7 +1372,7 @@ impl GamePacketHandler<'_> {
if *entity_id == p.player_id && dead.is_none() {
commands.entity(self.player).insert(Dead);
events.send(DeathEvent {
events.write(DeathEvent {
entity: self.player,
packet: Some(p.clone()),
});
@ -1386,7 +1388,7 @@ impl GamePacketHandler<'_> {
debug!("Got resource pack packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(ResourcePackEvent {
events.write(ResourcePackEvent {
entity: self.player,
id: p.id,
url: p.url.to_owned(),
@ -1409,6 +1411,7 @@ impl GamePacketHandler<'_> {
&mut InstanceHolder,
&GameProfileComponent,
&ClientInformation,
Option<&mut InstanceName>,
),
With<LocalEntity>,
>,
@ -1418,11 +1421,19 @@ impl GamePacketHandler<'_> {
)>(
self.ecs,
|(mut commands, mut query, mut events, mut instance_container, mut loaded_by_query)| {
let (mut instance_holder, game_profile, client_information) =
let (mut instance_holder, game_profile, client_information, instance_name) =
query.get_mut(self.player).unwrap();
let new_instance_name = p.common.dimension.clone();
if let Some(mut instance_name) = instance_name {
**instance_name = new_instance_name.clone();
} else {
commands
.entity(self.player)
.insert(InstanceName(new_instance_name.clone()));
}
let Some((_dimension_type, dimension_data)) = p
.common
.dimension_type(&instance_holder.instance.read().registries)
@ -1432,13 +1443,13 @@ impl GamePacketHandler<'_> {
// add this world to the instance_container (or don't if it's already
// there)
let weak_instance = instance_container.insert(
let weak_instance = instance_container.get_or_insert(
new_instance_name.clone(),
dimension_data.height,
dimension_data.min_y,
&instance_holder.instance.read().registries,
);
events.send(InstanceLoadedEvent {
events.write(InstanceLoadedEvent {
entity: self.player,
name: new_instance_name.clone(),
instance: Arc::downgrade(&weak_instance),
@ -1486,18 +1497,30 @@ impl GamePacketHandler<'_> {
pub fn start_configuration(&mut self, _p: &ClientboundStartConfiguration) {
debug!("Got start configuration packet");
as_system::<Commands>(self.ecs, |mut commands| {
commands.trigger(SendPacketEvent::new(
self.player,
ServerboundConfigurationAcknowledged,
));
as_system::<(Commands, Query<(&mut RawConnection, &mut InstanceHolder)>)>(
self.ecs,
|(mut commands, mut query)| {
let Some((mut raw_conn, mut instance_holder)) = query.get_mut(self.player).ok()
else {
warn!("Got start configuration packet but player doesn't have a RawConnection");
return;
};
raw_conn.state = ConnectionProtocol::Configuration;
commands
.entity(self.player)
.insert(crate::client::InConfigState)
.remove::<crate::JoinedClientBundle>()
.remove::<EntityBundle>();
});
commands.trigger(SendPacketEvent::new(
self.player,
ServerboundConfigurationAcknowledged,
));
commands
.entity(self.player)
.insert(crate::client::InConfigState)
.remove::<crate::JoinedClientBundle>()
.remove::<EntityBundle>();
instance_holder.reset();
},
);
}
pub fn entity_position_sync(&mut self, p: &ClientboundEntityPositionSync) {
@ -1555,7 +1578,9 @@ impl GamePacketHandler<'_> {
pub fn set_display_objective(&mut self, _p: &ClientboundSetDisplayObjective) {}
pub fn set_objective(&mut self, _p: &ClientboundSetObjective) {}
pub fn set_passengers(&mut self, _p: &ClientboundSetPassengers) {}
pub fn set_player_team(&mut self, _p: &ClientboundSetPlayerTeam) {}
pub fn set_player_team(&mut self, p: &ClientboundSetPlayerTeam) {
debug!("Got set player team packet {p:?}");
}
pub fn set_score(&mut self, _p: &ClientboundSetScore) {}
pub fn set_simulation_distance(&mut self, _p: &ClientboundSetSimulationDistance) {}
pub fn set_subtitle_text(&mut self, _p: &ClientboundSetSubtitleText) {}

View file

@ -72,7 +72,7 @@ impl LoginPacketHandler<'_> {
debug!("Got disconnect {:?}", p);
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(DisconnectEvent {
events.write(DisconnectEvent {
entity: self.player,
reason: Some(p.reason.clone()),
});
@ -121,7 +121,7 @@ impl LoginPacketHandler<'_> {
debug!("Got custom query {p:?}");
as_system::<EventWriter<ReceiveCustomQueryEvent>>(self.ecs, |mut events| {
events.send(ReceiveCustomQueryEvent {
events.write(ReceiveCustomQueryEvent {
entity: self.player,
packet: p.clone(),
disabled: false,

View file

@ -20,7 +20,7 @@ pub fn death_event_on_0_health(
) {
for (entity, health) in query.iter() {
if **health == 0. {
death_events.send(DeathEvent {
death_events.write(DeathEvent {
entity,
packet: None,
});

View file

@ -24,14 +24,14 @@ impl Plugin for PongPlugin {
pub fn reply_to_game_ping(trigger: Trigger<PingEvent>, mut commands: Commands) {
commands.trigger(SendPacketEvent::new(
trigger.entity(),
trigger.target(),
azalea_protocol::packets::game::ServerboundPong { id: trigger.0.id },
));
}
pub fn reply_to_config_ping(trigger: Trigger<ConfigPingEvent>, mut commands: Commands) {
commands.trigger(SendConfigPacketEvent::new(
trigger.entity(),
trigger.target(),
azalea_protocol::packets::config::ServerboundPong { id: trigger.0.id },
));
}

View file

@ -3,7 +3,7 @@
use std::marker::PhantomData;
use bevy_app::{App, Last, Plugin};
use bevy_ecs::system::{NonSend, Resource};
use bevy_ecs::prelude::*;
use bevy_tasks::{
AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder,
tick_global_task_pools_on_main_thread,

View file

@ -37,7 +37,8 @@ pub fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
pub fn send_update_broadcast(update_broadcast: ResMut<UpdateBroadcast>) {
let _ = update_broadcast.0.send(());
}
/// A plugin that makes the [`RanScheduleBroadcast`] resource available.
/// A plugin that makes the [`UpdateBroadcast`] and [`TickBroadcast`] resources
/// available.
pub struct TickBroadcastPlugin;
impl Plugin for TickBroadcastPlugin {
fn build(&self, app: &mut App) {

View file

@ -20,6 +20,7 @@ use azalea_registry::{DimensionType, EntityKind};
use azalea_world::palette::{PalettedContainer, PalettedContainerKind};
use azalea_world::{Chunk, Instance, MinecraftEntityId, Section};
use bevy_app::App;
use bevy_ecs::component::Mutable;
use bevy_ecs::{prelude::*, schedule::ExecutorKind};
use parking_lot::RwLock;
use simdnbt::owned::{NbtCompound, NbtTag};
@ -100,6 +101,11 @@ impl Simulation {
pub fn tick(&mut self) {
tick_app(&mut self.app);
}
pub fn minecraft_entity_id(&self) -> MinecraftEntityId {
self.component::<MinecraftEntityId>()
}
pub fn component<T: Component + Clone>(&self) -> T {
self.app.world().get::<T>(self.entity).unwrap().clone()
}
@ -109,7 +115,10 @@ impl Simulation {
pub fn has_component<T: Component>(&self) -> bool {
self.app.world().get::<T>(self.entity).is_some()
}
pub fn with_component_mut<T: Component>(&mut self, f: impl FnOnce(&mut T)) {
pub fn with_component_mut<T: Component<Mutability = Mutable>>(
&mut self,
f: impl FnOnce(&mut T),
) {
f(&mut self
.app
.world_mut()

View file

@ -0,0 +1,136 @@
use azalea_client::{InConfigState, InGameState, InstanceHolder, test_simulation::*};
use azalea_core::{position::ChunkPos, resource_location::ResourceLocation};
use azalea_entity::LocalEntity;
use azalea_protocol::packets::{
ConnectionProtocol, Packet,
config::{ClientboundFinishConfiguration, ClientboundRegistryData},
game::ClientboundStartConfiguration,
};
use azalea_registry::{DataRegistry, DimensionType};
use azalea_world::InstanceName;
use bevy_log::tracing_subscriber;
use simdnbt::owned::{NbtCompound, NbtTag};
#[test]
fn test_login_to_dimension_with_same_name() {
let _ = tracing_subscriber::fmt().try_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: DimensionType, instance_name: ResourceLocation| {
make_basic_respawn_packet(dimension, instance_name).into_variant()
}
} else {
|dimension: DimensionType, instance_name: ResourceLocation| {
make_basic_login_packet(dimension, instance_name).into_variant()
}
};
let _ = tracing_subscriber::fmt::try_init();
let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
assert!(simulation.has_component::<InConfigState>());
assert!(!simulation.has_component::<InGameState>());
simulation.receive_packet(ClientboundRegistryData {
registry_id: ResourceLocation::new("minecraft:dimension_type"),
entries: vec![(
ResourceLocation::new("minecraft:overworld"),
Some(NbtCompound::from_values(vec![
("height".into(), NbtTag::Int(384)),
("min_y".into(), NbtTag::Int(-64)),
])),
)]
.into_iter()
.collect(),
});
simulation.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(
DimensionType::new_raw(0), // overworld
ResourceLocation::new("azalea:overworld"),
));
simulation.tick();
assert_eq!(
*simulation.component::<InstanceName>(),
ResourceLocation::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: ResourceLocation::new("minecraft:dimension_type"),
entries: vec![(
ResourceLocation::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(
DimensionType::new_raw(0),
ResourceLocation::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>(),
ResourceLocation::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");
simulation.receive_packet(make_basic_login_or_respawn_packet(
DimensionType::new_raw(2), // nether
ResourceLocation::new("minecraft:nether"),
));
simulation.tick();
}

View file

@ -1,6 +1,6 @@
use crate::{
block_hit_result::BlockHitResult,
direction::{Axis, Direction},
hit_result::BlockHitResult,
math::EPSILON,
position::{BlockPos, Vec3},
};

View file

@ -1,34 +0,0 @@
use crate::{
direction::Direction,
position::{BlockPos, Vec3},
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BlockHitResult {
pub location: Vec3,
pub direction: Direction,
pub block_pos: BlockPos,
pub miss: bool,
pub inside: bool,
pub world_border: bool,
}
impl BlockHitResult {
pub fn miss(location: Vec3, direction: Direction, block_pos: BlockPos) -> Self {
Self {
location,
direction,
block_pos,
miss: true,
inside: false,
world_border: false,
}
}
pub fn with_direction(&self, direction: Direction) -> Self {
Self { direction, ..*self }
}
pub fn with_position(&self, block_pos: BlockPos) -> Self {
Self { block_pos, ..*self }
}
}

View file

@ -0,0 +1,68 @@
use crate::{
direction::Direction,
position::{BlockPos, Vec3},
};
/// The block or entity that our player is looking at and can interact with.
///
/// If there's nothing, it'll be a [`BlockHitResult`] with `miss` set to true.
#[derive(Debug, Clone, PartialEq)]
pub enum HitResult {
Block(BlockHitResult),
/// TODO
Entity,
}
impl HitResult {
pub fn is_miss(&self) -> bool {
match self {
HitResult::Block(block_hit_result) => block_hit_result.miss,
HitResult::Entity => false,
}
}
pub fn is_block_hit_and_not_miss(&self) -> bool {
match self {
HitResult::Block(block_hit_result) => !block_hit_result.miss,
HitResult::Entity => false,
}
}
/// Returns the [`BlockHitResult`], if we were looking at a block and it
/// wasn't a miss.
pub fn as_block_hit_result_if_not_miss(&self) -> Option<&BlockHitResult> {
match self {
HitResult::Block(block_hit_result) if !block_hit_result.miss => Some(block_hit_result),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BlockHitResult {
pub location: Vec3,
pub direction: Direction,
pub block_pos: BlockPos,
pub inside: bool,
pub world_border: bool,
pub miss: bool,
}
impl BlockHitResult {
pub fn miss(location: Vec3, direction: Direction, block_pos: BlockPos) -> Self {
Self {
location,
direction,
block_pos,
miss: true,
inside: false,
world_border: false,
}
}
pub fn with_direction(&self, direction: Direction) -> Self {
Self { direction, ..*self }
}
pub fn with_position(&self, block_pos: BlockPos) -> Self {
Self { block_pos, ..*self }
}
}

View file

@ -2,7 +2,6 @@
pub mod aabb;
pub mod bitset;
pub mod block_hit_result;
pub mod color;
pub mod cursor3d;
pub mod data_registry;
@ -11,6 +10,7 @@ pub mod difficulty;
pub mod direction;
pub mod filterable;
pub mod game_type;
pub mod hit_result;
pub mod math;
pub mod objectives;
pub mod position;

View file

@ -218,9 +218,9 @@ pub struct Jumping(pub bool);
/// A component that contains the direction an entity is looking.
#[derive(Debug, Component, Copy, Clone, Default, PartialEq, AzBuf)]
pub struct LookDirection {
/// Left and right. Aka yaw.
/// Left and right. AKA yaw.
pub y_rot: f32,
/// Up and down. Aka pitch.
/// Up and down. AKA pitch.
pub x_rot: f32,
}

View file

@ -7,12 +7,7 @@ use std::{
use azalea_core::position::ChunkPos;
use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{Added, Changed, Without},
system::{Commands, Query, Res, ResMut, Resource},
};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use nohash_hasher::IntMap;
use tracing::{debug, trace, warn};
@ -136,8 +131,9 @@ pub fn update_entity_chunk_positions(
mut query: Query<(Entity, &Position, &InstanceName, &mut EntityChunkPos), Changed<Position>>,
instance_container: Res<InstanceContainer>,
) {
for (entity, pos, world_name, mut entity_chunk_pos) in query.iter_mut() {
let instance_lock = instance_container.get(world_name).unwrap();
for (entity, pos, instance_name, mut entity_chunk_pos) in query.iter_mut() {
// TODO: move this inside of the if statement so it's not called as often
let instance_lock = instance_container.get(instance_name).unwrap();
let mut instance = instance_lock.write();
let old_chunk = **entity_chunk_pos;

View file

@ -18,12 +18,7 @@
use std::sync::Arc;
use azalea_world::{MinecraftEntityId, PartialInstance};
use bevy_ecs::{
prelude::{Component, Entity},
query::With,
system::{EntityCommand, Query},
world::{EntityWorldMut, World},
};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use parking_lot::RwLock;
use tracing::warn;
@ -68,19 +63,17 @@ impl RelativeEntityUpdate {
pub struct UpdatesReceived(u32);
impl EntityCommand for RelativeEntityUpdate {
fn apply(self, entity: Entity, world: &mut World) {
fn apply(self, mut entity: EntityWorldMut) {
let partial_entity_infos = &mut self.partial_world.write().entity_infos;
let mut entity_mut = world.entity_mut(entity);
if Some(entity) == partial_entity_infos.owner_entity {
if Some(entity.id()) == partial_entity_infos.owner_entity {
// if the entity owns this partial world, it's always allowed to update itself
(self.update)(&mut entity_mut);
(self.update)(&mut entity);
return;
};
let entity_id = *entity_mut.get::<MinecraftEntityId>().unwrap();
if entity_mut.contains::<LocalEntity>() {
let entity_id = *entity.get::<MinecraftEntityId>().unwrap();
if entity.contains::<LocalEntity>() {
// a client tried to update another client, which isn't allowed
return;
}
@ -90,7 +83,7 @@ impl EntityCommand for RelativeEntityUpdate {
.get(&entity_id)
.copied();
let can_update = if let Some(updates_received) = entity_mut.get::<UpdatesReceived>() {
let can_update = if let Some(updates_received) = entity.get::<UpdatesReceived>() {
this_client_updates_received.unwrap_or(1) == **updates_received
} else {
// no UpdatesReceived means the entity was just spawned
@ -102,9 +95,8 @@ impl EntityCommand for RelativeEntityUpdate {
.updates_received
.insert(entity_id, new_updates_received);
entity_mut.insert(UpdatesReceived(new_updates_received));
entity.insert(UpdatesReceived(new_updates_received));
let mut entity = world.entity_mut(entity);
(self.update)(&mut entity);
}
}

View file

@ -809,7 +809,7 @@ impl DataComponent for ContainerLoot {
#[derive(Clone, PartialEq, AzBuf)]
pub enum JukeboxPlayable {
Registry(registry::JukeboxSong),
Referenced(ResourceLocation),
Direct(Holder<registry::JukeboxSong, JukeboxSongData>),
}
impl DataComponent for JukeboxPlayable {
@ -1206,8 +1206,8 @@ pub struct ItemDamageFunction {
#[derive(Clone, PartialEq, AzBuf)]
pub enum ProvidesTrimMaterial {
Holder(Holder<TrimMaterial, DirectTrimMaterial>),
Registry(TrimMaterial),
Referenced(ResourceLocation),
Direct(Holder<TrimMaterial, DirectTrimMaterial>),
}
impl DataComponent for ProvidesTrimMaterial {
const KIND: DataComponentKind = DataComponentKind::ProvidesTrimMaterial;
@ -1263,7 +1263,7 @@ impl DataComponent for CowVariant {
#[derive(Clone, PartialEq, AzBuf)]
pub enum ChickenVariant {
Registry(azalea_registry::ChickenVariant),
Referenced(ResourceLocation),
Direct(ChickenVariantData),
}
impl DataComponent for ChickenVariant {
@ -1271,6 +1271,5 @@ impl DataComponent for ChickenVariant {
}
#[derive(Clone, PartialEq, AzBuf)]
pub struct ChickenVariantData {
// not a typo, as of 1.21.5 chicken variants do actually seem to be encoded like this
pub registry: azalea_registry::ChickenVariant,
}

View file

@ -382,6 +382,7 @@
"argument.range.swapped": "Min cannot be bigger than max",
"argument.resource_or_id.failed_to_parse": "Failed to parse structure: %s",
"argument.resource_or_id.invalid": "Invalid id or tag",
"argument.resource_or_id.no_such_element": "Can't find element '%s' in registry '%s'",
"argument.resource_selector.not_found": "No matches for selector '%s' of type '%s'",
"argument.resource_tag.invalid_type": "Tag '%s' has wrong type '%s' (expected '%s')",
"argument.resource_tag.not_found": "Can't find tag '%s' of type '%s'",
@ -2353,6 +2354,7 @@
"block.minecraft.zombie_head": "Zombie Head",
"block.minecraft.zombie_wall_head": "Zombie Wall Head",
"book.byAuthor": "by %1$s",
"book.edit.title": "Book Edit Screen",
"book.editTitle": "Enter Book Title:",
"book.finalizeButton": "Sign and Close",
"book.finalizeWarning": "Note! When you sign the book, it will no longer be editable.",
@ -2361,8 +2363,13 @@
"book.generation.2": "Copy of a copy",
"book.generation.3": "Tattered",
"book.invalid.tag": "* Invalid book tag *",
"book.page_button.next": "Next Page",
"book.page_button.previous": "Previous Page",
"book.pageIndicator": "Page %1$s of %2$s",
"book.sign.title": "Book Sign Screen",
"book.sign.titlebox": "Title",
"book.signButton": "Sign",
"book.view.title": "Book View Screen",
"build.tooHigh": "Height limit for building is %s",
"chat_screen.message": "Message to send: %s",
"chat_screen.title": "Chat screen",
@ -2955,7 +2962,7 @@
"commands.waypoint.list.success": "%s waypoint(s) in %s: %s",
"commands.waypoint.modify.color": "Waypoint color is now %s",
"commands.waypoint.modify.color.reset": "Reset waypoint color",
"commands.waypoint.modify.fade": "Waypoint fade changed",
"commands.waypoint.modify.style": "Waypoint style changed",
"commands.weather.set.clear": "Set the weather to clear",
"commands.weather.set.rain": "Set the weather to rain",
"commands.weather.set.thunder": "Set the weather to rain & thunder",
@ -3685,6 +3692,8 @@
"gamerule.keepInventory": "Keep inventory after death",
"gamerule.lavaSourceConversion": "Lava converts to source",
"gamerule.lavaSourceConversion.description": "When flowing lava is surrounded on two sides by lava sources it converts into a source.",
"gamerule.locatorBar": "Enable player Locator Bar",
"gamerule.locatorBar.description": "When enabled, a bar is shown on the screen to indicate the direction of players.",
"gamerule.logAdminCommands": "Broadcast admin commands",
"gamerule.maxCommandChainLength": "Command chain size limit",
"gamerule.maxCommandChainLength.description": "Applies to command block chains and functions.",
@ -3722,8 +3731,6 @@
"gamerule.tntExplosionDropDecay.description": "Some of the drops from blocks destroyed by explosions caused by TNT are lost in the explosion.",
"gamerule.universalAnger": "Universal anger",
"gamerule.universalAnger.description": "Angered neutral mobs attack any nearby player, not just the player that angered them. Works best if forgiveDeadPlayers is disabled.",
"gamerule.useLocatorBar": "Use player Locator Bar",
"gamerule.useLocatorBar.description": "When enabled, a bar is shown on the screen to indicate the direction of players.",
"gamerule.waterSourceConversion": "Water converts to source",
"gamerule.waterSourceConversion.description": "When flowing water is surrounded on two sides by water sources it converts into a source.",
"generator.custom": "Custom",
@ -4960,22 +4967,26 @@
"mco.configure.world.activityfeed.disabled": "Player feed temporarily disabled",
"mco.configure.world.backup": "World Backups",
"mco.configure.world.buttons.activity": "Player activity",
"mco.configure.world.buttons.close": "Close Realm",
"mco.configure.world.buttons.close": "Temporarily Close Realm",
"mco.configure.world.buttons.delete": "Delete",
"mco.configure.world.buttons.done": "Done",
"mco.configure.world.buttons.edit": "Settings",
"mco.configure.world.buttons.invite": "Invite Player",
"mco.configure.world.buttons.moreoptions": "More options",
"mco.configure.world.buttons.open": "Open Realm",
"mco.configure.world.buttons.newworld": "New World",
"mco.configure.world.buttons.open": "Reopen Realm",
"mco.configure.world.buttons.options": "World Options",
"mco.configure.world.buttons.players": "Players",
"mco.configure.world.buttons.region_preference": "Select Region...",
"mco.configure.world.buttons.resetworld": "Reset World",
"mco.configure.world.buttons.save": "Save",
"mco.configure.world.buttons.settings": "Settings",
"mco.configure.world.buttons.subscription": "Subscription",
"mco.configure.world.buttons.switchminigame": "Switch Minigame",
"mco.configure.world.close.question.line1": "Your Realm will become unavailable.",
"mco.configure.world.close.question.line1": "You can temporarily close your Realm, preventing play while you make adjustments. Open it back up when you're ready. \n\nThis does not cancel your Realms Subscription.",
"mco.configure.world.close.question.line2": "Are you sure you want to continue?",
"mco.configure.world.closing": "Closing the Realm...",
"mco.configure.world.close.question.title": "Need to make changes without disruption?",
"mco.configure.world.closing": "Temporarily closing the Realm...",
"mco.configure.world.commandBlocks": "Command Blocks",
"mco.configure.world.delete.button": "Delete Realm",
"mco.configure.world.delete.question.line1": "Your Realm will be permanently deleted",
@ -5003,6 +5014,8 @@
"mco.configure.world.players.inviting": "Inviting player...",
"mco.configure.world.players.title": "Players",
"mco.configure.world.pvp": "PVP",
"mco.configure.world.region_preference": "Region Preference",
"mco.configure.world.region_preference.title": "Region Preference Selection",
"mco.configure.world.reset.question.line1": "Your world will be regenerated and your current world will be lost",
"mco.configure.world.reset.question.line2": "Are you sure you want to continue?",
"mco.configure.world.resourcepack.question": "You need a custom resource pack to play on this Realm\n\nDo you want to download it and play?",
@ -5012,6 +5025,7 @@
"mco.configure.world.restore.download.question.line2": "Do you want to continue?",
"mco.configure.world.restore.question.line1": "Your world will be restored to date '%s' (%s)",
"mco.configure.world.restore.question.line2": "Are you sure you want to continue?",
"mco.configure.world.settings.expired": "You cannot edit settings of an expired Realm",
"mco.configure.world.settings.title": "Settings",
"mco.configure.world.slot": "World %s",
"mco.configure.world.slot.empty": "Empty",
@ -5041,6 +5055,7 @@
"mco.configure.world.subscription.remaining.months": "%1$s month(s)",
"mco.configure.world.subscription.remaining.months.days": "%1$s month(s), %2$s day(s)",
"mco.configure.world.subscription.start": "Start Date",
"mco.configure.world.subscription.tab": "Subscription",
"mco.configure.world.subscription.timeleft": "Time Left",
"mco.configure.world.subscription.title": "Your Subscription",
"mco.configure.world.subscription.unknown": "Unknown",
@ -5091,6 +5106,7 @@
"mco.errorMessage.initialize.failed": "Failed to initialize Realm",
"mco.errorMessage.noDetails": "No error details provided",
"mco.errorMessage.realmsService": "An error occurred (%s):",
"mco.errorMessage.realmsService.configurationError": "An unexpected error occurred while editing world options",
"mco.errorMessage.realmsService.connectivity": "Could not connect to Realms: %s",
"mco.errorMessage.realmsService.realmsError": "Realms (%s):",
"mco.errorMessage.realmsService.unknownCompatibility": "Could not check compatible version, got response: %s",
@ -5140,7 +5156,7 @@
"mco.reset.world.warning": "This will replace the current world of your Realm",
"mco.selectServer.buy": "Buy a Realm!",
"mco.selectServer.close": "Close",
"mco.selectServer.closed": "Closed Realm",
"mco.selectServer.closed": "Deactivated Realm",
"mco.selectServer.closeserver": "Close Realm",
"mco.selectServer.configure": "Configure",
"mco.selectServer.configureRealm": "Configure Realm",
@ -5618,6 +5634,7 @@
"options.realmsNotifications.tooltip": "Fetches Realms news and invites in the title screen and displays their respective icon on the Realms button.",
"options.reducedDebugInfo": "Reduced Debug Info",
"options.renderClouds": "Clouds",
"options.renderCloudsDistance": "Cloud Distance",
"options.renderDistance": "Render Distance",
"options.resourcepack": "Resource Packs...",
"options.rotateWithMinecart": "Rotate with Minecarts",
@ -5804,6 +5821,31 @@
"quickplay.error.realm_connect": "Could not connect to Realm",
"quickplay.error.realm_permission": "Lacking permission to connect to this Realm",
"quickplay.error.title": "Failed to Quick Play",
"realms.configuration.region_preference.automatic_owner": "Automatic (Realm owner ping)",
"realms.configuration.region_preference.automatic_player": "Automatic (first to join session)",
"realms.configuration.region.australia_east": "New South Wales, Australia",
"realms.configuration.region.australia_southeast": "Victoria, Australia",
"realms.configuration.region.brazil_south": "Brazil",
"realms.configuration.region.central_india": "India",
"realms.configuration.region.central_us": "Iowa, USA",
"realms.configuration.region.east_asia": "Hong Kong",
"realms.configuration.region.east_us": "Virginia, USA",
"realms.configuration.region.east_us_2": "North Carolina, USA",
"realms.configuration.region.france_central": "France",
"realms.configuration.region.japan_east": "Eastern Japan",
"realms.configuration.region.japan_west": "Western Japan",
"realms.configuration.region.korea_central": "South Korea",
"realms.configuration.region.north_central_us": "Illinois, USA",
"realms.configuration.region.north_europe": "Ireland",
"realms.configuration.region.south_central_us": "Texas, USA",
"realms.configuration.region.southeast_asia": "Singapore",
"realms.configuration.region.sweden_central": "Sweden",
"realms.configuration.region.uae_north": "United Arab Emirates (UAE)",
"realms.configuration.region.uk_south": "Southern England",
"realms.configuration.region.west_central_us": "Utah, USA",
"realms.configuration.region.west_europe": "Netherlands",
"realms.configuration.region.west_us": "California, USA",
"realms.configuration.region.west_us_2": "Washington, USA",
"realms.missing.snapshot.error.text": "Realms is currently not supported in snapshots",
"recipe.notFound": "Unknown recipe: %s",
"recipe.toast.description": "Check your recipe book",

View file

@ -6,8 +6,8 @@ use azalea_block::{
};
use azalea_core::{
aabb::AABB,
block_hit_result::BlockHitResult,
direction::{Axis, Direction},
hit_result::BlockHitResult,
math::{self, EPSILON, lerp},
position::{BlockPos, Vec3},
};
@ -111,9 +111,11 @@ pub fn clip(chunk_storage: &ChunkStorage, context: ClipContext) -> BlockHitResul
let fluid_clip = fluid_shape.clip(&ctx.from, &ctx.to, block_pos);
let distance_to_interaction = interaction_clip
.as_ref()
.map(|hit| ctx.from.distance_squared_to(&hit.location))
.unwrap_or(f64::MAX);
let distance_to_fluid = fluid_clip
.as_ref()
.map(|hit| ctx.from.distance_squared_to(&hit.location))
.unwrap_or(f64::MAX);

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
use std::{cmp, num::NonZeroU32, sync::LazyLock};
use azalea_core::{
block_hit_result::BlockHitResult,
direction::{Axis, AxisCycle, Direction},
hit_result::BlockHitResult,
math::{EPSILON, binary_search},
position::{BlockPos, Vec3},
};

View file

@ -20,13 +20,7 @@ use azalea_entity::{
};
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_app::{App, Plugin};
use bevy_ecs::{
entity::Entity,
query::With,
schedule::{IntoSystemConfigs, SystemSet},
system::{Query, Res},
world::Mut,
};
use bevy_ecs::prelude::*;
use clip::box_traverse_blocks;
use collision::{
BLOCK_SHAPE, BlockWithShape, MoverType, VoxelShape,

View file

@ -26,12 +26,14 @@ fn make_test_app() -> App {
}
pub fn insert_overworld(app: &mut App) -> Arc<RwLock<Instance>> {
app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
)
app.world_mut()
.resource_mut::<InstanceContainer>()
.get_or_insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
)
}
#[test]
@ -248,12 +250,15 @@ fn test_top_slab_collision() {
#[test]
fn test_weird_wall_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
);
let world_lock = app
.world_mut()
.resource_mut::<InstanceContainer>()
.get_or_insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
@ -307,12 +312,15 @@ fn test_weird_wall_collision() {
#[test]
fn test_negative_coordinates_weird_wall_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
);
let world_lock = app
.world_mut()
.resource_mut::<InstanceContainer>()
.get_or_insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
@ -370,12 +378,15 @@ fn test_negative_coordinates_weird_wall_collision() {
#[test]
fn spawn_and_unload_world() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
);
let world_lock = app
.world_mut()
.resource_mut::<InstanceContainer>()
.get_or_insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(

View file

@ -46,7 +46,7 @@ thiserror.workspace = true
tokio = { workspace = true, features = ["io-util", "net", "macros"] }
tokio-util = { workspace = true, features = ["codec"] }
tracing.workspace = true
hickory-resolver = { workspace = true, features = ["tokio"] }
hickory-resolver = { workspace = true, features = ["tokio", "system-config"] }
uuid.workspace = true
crc32fast = { workspace = true, optional = true }

View file

@ -1,26 +1,5 @@
# Azalea Protocol
A low-level crate to send and receive Minecraft packets. You should probably use `azalea` or `azalea-client` instead.
A low-level crate to send and receive Minecraft packets. If you intend on making bots, you should use the main `azalea` crate instead.
The goal is to only support the latest Minecraft version in order to ease development.
This is not yet complete, search for `TODO` in the code for things that need to be done.
Unfortunately, using azalea-protocol requires Rust nightly because [specialization](https://github.com/rust-lang/rust/issues/31844) is not stable yet. Use `rustup default nightly` to enable it.
## Adding a new packet
Adding new packets is usually pretty easy, but you'll want to have Minecraft's decompiled source code which you can obtain with tools such as [DecompilerMC](https://github.com/hube12/DecompilerMC).
1. First, you'll need the packet id. You can get this from azalea-protocol error messages or from [the wiki](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge).
2. Run `python codegen/newpacket.py [packet id] [clientbound or serverbound] \[game/handshake/login/status\]`\
3. Go to the directory where it told you the packet was generated. If there's no comments, you're done. Otherwise, keep going.
4. Find the packet in Minecraft's source code. Minecraft's packets are in the `net/minecraft/network/protocol/<state>` directory. The state for your packet is usually `game`.
5. Add the fields from Minecraft's source code from either the read or write methods. You can look at [the wiki](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol) if you're not sure about how a packet is structured, but be aware that the wiki uses different names for most things.
6. Format the code, submit a pull request, and wait for it to be reviewed.
### Implementing packets
You can manually implement reading and writing functionality for a packet by implementing AzaleaRead and AzaleaWrite, but you can also have this automatically generated for a struct or enum by deriving AzBuf.
Look at other packets as an example.
Only the latest Minecraft version is supported.

View file

@ -16,7 +16,7 @@ pub struct PositionMoveRotation {
pub look_direction: LookDirection,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct RelativeMovements {
pub x: bool,
pub y: bool,

View file

@ -1,6 +1,6 @@
//! Connect to remote servers/clients.
use std::fmt::Debug;
use std::fmt::{self, Debug, Display};
use std::io::{self, Cursor};
use std::marker::PhantomData;
use std::net::SocketAddr;
@ -262,6 +262,7 @@ pub enum ConnectionError {
use socks5_impl::protocol::UserKey;
/// An address and authentication method for connecting to a Socks5 proxy.
#[derive(Debug, Clone)]
pub struct Proxy {
pub addr: SocketAddr,
@ -273,6 +274,15 @@ impl Proxy {
Self { addr, auth }
}
}
impl Display for Proxy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "socks5://")?;
if let Some(auth) = &self.auth {
write!(f, "{auth}@")?;
}
write!(f, "{}", self.addr)
}
}
impl Connection<ClientboundHandshakePacket, ServerboundHandshakePacket> {
/// Create a new connection to the given address.

View file

@ -197,7 +197,7 @@ mod tests {
let buf = compression_encoder(&buf, compression_threshold).unwrap();
println!("{:?}", buf);
println!("{buf:?}");
compression_decoder(&mut Cursor::new(&buf), compression_threshold).unwrap();
}

View file

@ -1,9 +0,0 @@
use azalea_buf::AzBuf;
use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)]
pub struct ClientboundChatPreview {
pub query_id: i32,
pub preview: Option<FormattedText>,
}

View file

@ -29,7 +29,7 @@ mod tests {
];
let mut buf = Cursor::new(contents.as_slice());
let packet = ClientboundContainerSetContent::azalea_read(&mut buf).unwrap();
println!("{:?}", packet);
println!("{packet:?}");
assert_eq!(buf.position(), contents.len() as u64);

View file

@ -1,28 +0,0 @@
use azalea_buf::AzBuf;
use azalea_core::resource_location::ResourceLocation;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)]
pub struct ClientboundCustomSound {
pub name: ResourceLocation,
pub source: SoundSource,
pub x: i32,
pub y: i32,
pub z: i32,
pub volume: f32,
pub pitch: f32,
}
#[derive(AzBuf, Clone, Copy, Debug)]
pub enum SoundSource {
Master = 0,
Music = 1,
Records = 2,
Weather = 3,
Blocks = 4,
Hostile = 5,
Neutral = 6,
Players = 7,
Ambient = 8,
Voice = 9,
}

View file

@ -1,10 +0,0 @@
use azalea_buf::AzBuf;
use azalea_crypto::{MessageSignature, SignedMessageHeader};
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)]
pub struct ClientboundPlayerChatHeader {
pub header: SignedMessageHeader,
pub header_signature: MessageSignature,
pub body_digest: Vec<u8>,
}

View file

@ -28,7 +28,7 @@ mod tests {
];
let mut buf = Cursor::new(contents.as_slice());
let packet = ClientboundServerLinks::azalea_read(&mut buf).unwrap();
println!("{:?}", packet);
println!("{packet:?}");
assert_eq!(buf.position(), contents.len() as u64);
}

View file

@ -1,7 +0,0 @@
use azalea_buf::AzBuf;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)]
pub struct ClientboundSetDisplayChatPreview {
pub enabled: bool,
}

View file

@ -23,7 +23,7 @@ mod tests {
let contents = [161, 226, 1, 10, 18, 1, 20, 38, 124, 175, 198, 255];
let mut buf = Cursor::new(contents.as_slice());
let packet = ClientboundSetEntityData::azalea_read(&mut buf).unwrap();
println!("{:?}", packet);
println!("{packet:?}");
assert_eq!(buf.position(), contents.len() as u64);
@ -44,7 +44,7 @@ mod tests {
];
let mut buf = Cursor::new(contents.as_slice());
let packet = ClientboundSetEntityData::azalea_read(&mut buf).unwrap();
println!("{:?}", packet);
println!("{packet:?}");
assert_eq!(buf.position(), contents.len() as u64);
}

View file

@ -1,7 +1,5 @@
use std::io::{Cursor, Write};
use azalea_buf::{AzBuf, AzaleaRead, AzaleaWrite, BufReadError};
use azalea_chat::{style::ChatFormatting, FormattedText};
use azalea_buf::AzBuf;
use azalea_chat::{FormattedText, style::ChatFormatting};
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)]
@ -10,7 +8,7 @@ pub struct ClientboundSetPlayerTeam {
pub method: Method,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, AzBuf)]
pub enum Method {
Add((Parameters, PlayerList)),
Remove,
@ -19,56 +17,54 @@ pub enum Method {
Leave(PlayerList),
}
impl AzaleaRead for Method {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
Ok(match u8::azalea_read(buf)? {
0 => Method::Add((Parameters::azalea_read(buf)?, PlayerList::azalea_read(buf)?)),
1 => Method::Remove,
2 => Method::Change(Parameters::azalea_read(buf)?),
3 => Method::Join(PlayerList::azalea_read(buf)?),
4 => Method::Leave(PlayerList::azalea_read(buf)?),
id => return Err(BufReadError::UnexpectedEnumVariant { id: i32::from(id) }),
})
}
}
impl AzaleaWrite for Method {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
match self {
Method::Add((parameters, playerlist)) => {
0u8.azalea_write(buf)?;
parameters.azalea_write(buf)?;
playerlist.azalea_write(buf)?;
}
Method::Remove => {
1u8.azalea_write(buf)?;
}
Method::Change(parameters) => {
2u8.azalea_write(buf)?;
parameters.azalea_write(buf)?;
}
Method::Join(playerlist) => {
3u8.azalea_write(buf)?;
playerlist.azalea_write(buf)?;
}
Method::Leave(playerlist) => {
4u8.azalea_write(buf)?;
playerlist.azalea_write(buf)?;
}
}
Ok(())
}
}
#[derive(AzBuf, Clone, Debug)]
#[derive(Clone, Debug, AzBuf)]
pub struct Parameters {
pub display_name: FormattedText,
pub options: u8,
pub nametag_visibility: String,
pub collision_rule: String,
pub nametag_visibility: NameTagVisibility,
pub collision_rule: CollisionRule,
pub color: ChatFormatting,
pub player_prefix: FormattedText,
pub player_suffix: FormattedText,
}
#[derive(Clone, Copy, Debug, AzBuf)]
pub enum CollisionRule {
Always,
Never,
PushOtherTeams,
PushOwnTeam,
}
#[derive(Clone, Copy, Debug, AzBuf)]
pub enum NameTagVisibility {
Always,
Never,
HideForOtherTeams,
HideForOwnTeam,
}
type PlayerList = Vec<String>;
#[cfg(test)]
mod tests {
use std::io::Cursor;
use azalea_buf::AzaleaRead;
use crate::packets::game::ClientboundSetPlayerTeam;
#[test]
fn test_read_set_player_team() {
let contents = [
16, 99, 111, 108, 108, 105, 100, 101, 82, 117, 108, 101, 95, 57, 52, 53, 54, 0, 8, 0,
16, 99, 111, 108, 108, 105, 100, 101, 82, 117, 108, 101, 95, 57, 52, 53, 54, 1, 0, 1,
21, 8, 0, 0, 8, 0, 0, 0,
];
let mut buf = Cursor::new(contents.as_slice());
let packet = ClientboundSetPlayerTeam::azalea_read(&mut buf).unwrap();
println!("{packet:?}");
assert_eq!(buf.position(), contents.len() as u64);
}
}

View file

@ -47,7 +47,7 @@ mod tests {
];
let mut buf = Cursor::new(contents.as_slice());
let packet = ClientboundSound::azalea_read(&mut buf).unwrap();
println!("{:?}", packet);
println!("{packet:?}");
assert_eq!(buf.position(), contents.len() as u64);

View file

@ -56,4 +56,18 @@ mod tests {
"Position in queue: 328\nYou can purchase priority queue status to join the server faster, visit shop.2b2t.org".to_string()
);
}
#[test]
fn test_skyblock_net_message() {
#[rustfmt::skip]
let bytes = [
10, 8, 0, 4, 116, 101, 120, 116, 0, 0, 8, 0, 9, 105, 110, 115, 101, 114, 116, 105, 111, 110, 0, 7, 109, 97, 116, 115, 99, 97, 110, 9, 0, 5, 101, 120, 116, 114, 97, 10, 0, 0, 0, 2, 8, 0, 4, 116, 101, 120, 116, 0, 15, 194, 167, 97, 109, 97, 116, 115, 99, 97, 110, 194, 167, 114, 58, 32, 10, 0, 11, 104, 111, 118, 101, 114, 95, 101, 118, 101, 110, 116, 8, 0, 6, 97, 99, 116, 105, 111, 110, 0, 9, 115, 104, 111, 119, 95, 116, 101, 120, 116, 10, 0, 5, 118, 97, 108, 117, 101, 8, 0, 4, 116, 101, 120, 116, 0, 0, 9, 0, 5, 101, 120, 116, 114, 97, 8, 0, 0, 0, 1, 0, 126, 194, 167, 97, 109, 97, 116, 115, 99, 97, 110, 10, 194, 167, 101, 82, 97, 110, 107, 58, 32, 194, 167, 102, 194, 167, 97, 83, 107, 121, 98, 108, 111, 99, 107, 101, 114, 10, 194, 167, 54, 76, 101, 118, 101, 108, 58, 32, 194, 167, 102, 48, 10, 194, 167, 97, 74, 111, 105, 110, 101, 100, 58, 32, 194, 167, 102, 52, 109, 32, 52, 51, 115, 32, 97, 103, 111, 10, 194, 167, 98, 80, 108, 97, 121, 116, 105, 109, 101, 58, 32, 194, 167, 102, 50, 51, 115, 10, 194, 167, 55, 194, 167, 111, 42, 67, 108, 105, 99, 107, 32, 116, 111, 32, 118, 105, 115, 105, 116, 32, 105, 115, 108, 97, 110, 100, 42, 0, 0, 0, 8, 0, 0, 0, 4, 109, 101, 111, 119, 0, 10, 0, 11, 99, 108, 105, 99, 107, 95, 101, 118, 101, 110, 116, 8, 0, 6, 97, 99, 116, 105, 111, 110, 0, 15, 115, 117, 103, 103, 101, 115, 116, 95, 99, 111, 109, 109, 97, 110, 100, 8, 0, 5, 118, 97, 108, 117, 101, 0, 14, 47, 118, 105, 115, 105, 116, 32, 109, 97, 116, 115, 99, 97, 110, 8, 0, 7, 99, 111, 109, 109, 97, 110, 100, 0, 14, 47, 118, 105, 115, 105, 116, 32, 109, 97, 116, 115, 99, 97, 110, 0, 0, 0
];
let packet = ClientboundSystemChat::azalea_read(&mut Cursor::new(&bytes)).unwrap();
assert_eq!(
packet.content.to_ansi(),
"\u{1b}[38;2;85;255;85mmatscan\u{1b}[m\u{1b}[38;2;255;255;255m: meow\u{1b}[m"
)
}
}

View file

@ -1,8 +0,0 @@
use azalea_buf::AzBuf;
use azalea_core::resource_location::ResourceLocation;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)]
pub struct ClientboundUpdateEnabledFeatures {
pub features: Vec<ResourceLocation>,
}

View file

@ -80,8 +80,9 @@ impl AzaleaRead for ActionType {
}
}
#[derive(AzBuf, Clone, Copy, Debug)]
#[derive(AzBuf, Clone, Copy, Debug, Default)]
pub enum InteractionHand {
#[default]
MainHand = 0,
OffHand = 1,
}

View file

@ -17,7 +17,7 @@ pub struct ServerboundTestInstanceBlockAction {
pub enum Action {
#[default]
Init,
Qurey,
Query,
Set,
Reset,
Save,

View file

@ -8,6 +8,6 @@ pub struct ServerboundUseItem {
pub hand: InteractionHand,
#[var]
pub sequence: u32,
pub yaw: f32,
pub pitch: f32,
pub y_rot: f32,
pub x_rot: f32,
}

View file

@ -3,6 +3,7 @@ use std::io::{Cursor, Write};
use azalea_buf::{AzBuf, AzaleaRead, AzaleaWrite, BufReadError};
use azalea_core::{
direction::Direction,
hit_result::BlockHitResult,
position::{BlockPos, Vec3},
};
use azalea_protocol_macros::ServerboundGamePacket;
@ -77,3 +78,19 @@ impl AzaleaRead for BlockHit {
})
}
}
impl From<&BlockHitResult> for BlockHit {
/// Converts a [`BlockHitResult`] to a [`BlockHit`].
///
/// The only difference is that the `miss` field is not present in
/// [`BlockHit`].
fn from(hit_result: &BlockHitResult) -> Self {
Self {
block_pos: hit_result.block_pos,
direction: hit_result.direction,
location: hit_result.location,
inside: hit_result.inside,
world_border: hit_result.world_border,
}
}
}

View file

@ -11,8 +11,8 @@ use azalea_buf::{AzaleaReadVar, AzaleaWrite, AzaleaWriteVar, BufReadError};
use crate::read::ReadPacketError;
pub const PROTOCOL_VERSION: i32 = 1073742070;
pub const VERSION_NAME: &str = "25w16a";
pub const PROTOCOL_VERSION: i32 = 1073742073;
pub const VERSION_NAME: &str = "25w19a";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConnectionProtocol {

View file

@ -3,9 +3,7 @@
use std::net::{IpAddr, SocketAddr};
use async_recursion::async_recursion;
use hickory_resolver::{
Name, TokioResolver, config::ResolverConfig, name_server::TokioConnectionProvider,
};
use hickory_resolver::{Name, TokioResolver, name_server::TokioConnectionProvider};
use thiserror::Error;
use crate::ServerAddress;
@ -31,11 +29,9 @@ pub async fn resolve_address(address: &ServerAddress) -> Result<SocketAddr, Reso
// we specify Cloudflare instead of the default resolver because
// hickory_resolver has an issue on Windows where it's really slow using the
// default resolver
let resolver = TokioResolver::builder_with_config(
ResolverConfig::cloudflare(),
TokioConnectionProvider::default(),
)
.build();
let resolver = TokioResolver::builder(TokioConnectionProvider::default())
.unwrap()
.build();
// first, we do a srv lookup for _minecraft._tcp.<host>
let srv_redirect_result = resolver

View file

@ -4333,7 +4333,6 @@ enum SoundEvent {
BlockDriedGhastBreak => "minecraft:block.dried_ghast.break",
BlockDriedGhastStep => "minecraft:block.dried_ghast.step",
BlockDriedGhastFall => "minecraft:block.dried_ghast.fall",
BlockDriedGhastHit => "minecraft:block.dried_ghast.hit",
BlockDriedGhastAmbient => "minecraft:block.dried_ghast.ambient",
BlockDriedGhastAmbientWater => "minecraft:block.dried_ghast.ambient_water",
BlockDriedGhastPlace => "minecraft:block.dried_ghast.place",

View file

@ -148,7 +148,6 @@ pub static AZALEA_GROWS_ON: LazyLock<HashSet<Block>> = LazyLock::new(|| {
Block::Sand,
Block::RedSand,
Block::SuspiciousSand,
Block::SuspiciousSand,
Block::Terracotta,
Block::WhiteTerracotta,
Block::OrangeTerracotta,
@ -233,7 +232,6 @@ pub static BAMBOO_PLANTABLE_ON: LazyLock<HashSet<Block>> = LazyLock::new(|| {
Block::Sand,
Block::RedSand,
Block::SuspiciousSand,
Block::SuspiciousSand,
Block::Dirt,
Block::GrassBlock,
Block::Podzol,
@ -432,7 +430,6 @@ pub static CAMEL_SAND_STEP_SOUND_BLOCKS: LazyLock<HashSet<Block>> = LazyLock::ne
Block::Sand,
Block::RedSand,
Block::SuspiciousSand,
Block::SuspiciousSand,
Block::WhiteConcretePowder,
Block::OrangeConcretePowder,
Block::MagentaConcretePowder,
@ -451,14 +448,8 @@ pub static CAMEL_SAND_STEP_SOUND_BLOCKS: LazyLock<HashSet<Block>> = LazyLock::ne
Block::BlackConcretePowder,
])
});
pub static CAMELS_SPAWNABLE_ON: LazyLock<HashSet<Block>> = LazyLock::new(|| {
HashSet::from_iter(vec![
Block::Sand,
Block::RedSand,
Block::SuspiciousSand,
Block::SuspiciousSand,
])
});
pub static CAMELS_SPAWNABLE_ON: LazyLock<HashSet<Block>> =
LazyLock::new(|| HashSet::from_iter(vec![Block::Sand, Block::RedSand, Block::SuspiciousSand]));
pub static CAMPFIRES: LazyLock<HashSet<Block>> =
LazyLock::new(|| HashSet::from_iter(vec![Block::Campfire, Block::SoulCampfire]));
pub static CANDLE_CAKES: LazyLock<HashSet<Block>> = LazyLock::new(|| {
@ -848,7 +839,6 @@ pub static DRY_VEGETATION_MAY_PLACE_ON: LazyLock<HashSet<Block>> = LazyLock::new
Block::Sand,
Block::RedSand,
Block::SuspiciousSand,
Block::SuspiciousSand,
Block::Terracotta,
Block::WhiteTerracotta,
Block::OrangeTerracotta,
@ -1185,6 +1175,16 @@ pub static GUARDED_BY_PIGLINS: LazyLock<HashSet<Block>> = LazyLock::new(|| {
Block::DeepslateGoldOre,
])
});
pub static HAPPY_GHAST_AVOIDS: LazyLock<HashSet<Block>> = LazyLock::new(|| {
HashSet::from_iter(vec![
Block::SweetBerryBush,
Block::Cactus,
Block::WitherRose,
Block::MagmaBlock,
Block::Fire,
Block::PointedDripstone,
])
});
pub static HOGLIN_REPELLENTS: LazyLock<HashSet<Block>> = LazyLock::new(|| {
HashSet::from_iter(vec![
Block::WarpedFungus,
@ -2768,7 +2768,6 @@ pub static OVERWORLD_CARVER_REPLACEABLES: LazyLock<HashSet<Block>> = LazyLock::n
Block::Sand,
Block::RedSand,
Block::SuspiciousSand,
Block::SuspiciousSand,
Block::Terracotta,
Block::WhiteTerracotta,
Block::OrangeTerracotta,
@ -3106,14 +3105,8 @@ pub static REPLACEABLE_BY_TREES: LazyLock<HashSet<Block>> = LazyLock::new(|| {
Block::ClosedEyeblossom,
])
});
pub static SAND: LazyLock<HashSet<Block>> = LazyLock::new(|| {
HashSet::from_iter(vec![
Block::Sand,
Block::RedSand,
Block::SuspiciousSand,
Block::SuspiciousSand,
])
});
pub static SAND: LazyLock<HashSet<Block>> =
LazyLock::new(|| HashSet::from_iter(vec![Block::Sand, Block::RedSand, Block::SuspiciousSand]));
pub static SAPLINGS: LazyLock<HashSet<Block>> = LazyLock::new(|| {
HashSet::from_iter(vec![
Block::OakSapling,
@ -3640,6 +3633,8 @@ pub static TRIGGERS_AMBIENT_DESERT_DRY_VEGETATION_BLOCK_SOUNDS: LazyLock<HashSet
});
pub static TRIGGERS_AMBIENT_DESERT_SAND_BLOCK_SOUNDS: LazyLock<HashSet<Block>> =
LazyLock::new(|| HashSet::from_iter(vec![Block::Sand, Block::RedSand]));
pub static TRIGGERS_AMBIENT_DRIED_GHAST_BLOCK_SOUNDS: LazyLock<HashSet<Block>> =
LazyLock::new(|| HashSet::from_iter(vec![Block::SoulSand, Block::SoulSoil]));
pub static UNDERWATER_BONEMEALS: LazyLock<HashSet<Block>> = LazyLock::new(|| {
HashSet::from_iter(vec![
Block::Seagrass,

View file

@ -1506,14 +1506,8 @@ pub static REPAIRS_TURTLE_HELMET: LazyLock<HashSet<Item>> =
LazyLock::new(|| HashSet::from_iter(vec![Item::TurtleScute]));
pub static REPAIRS_WOLF_ARMOR: LazyLock<HashSet<Item>> =
LazyLock::new(|| HashSet::from_iter(vec![Item::ArmadilloScute]));
pub static SAND: LazyLock<HashSet<Item>> = LazyLock::new(|| {
HashSet::from_iter(vec![
Item::Sand,
Item::RedSand,
Item::SuspiciousSand,
Item::SuspiciousSand,
])
});
pub static SAND: LazyLock<HashSet<Item>> =
LazyLock::new(|| HashSet::from_iter(vec![Item::Sand, Item::RedSand, Item::SuspiciousSand]));
pub static SAPLINGS: LazyLock<HashSet<Item>> = LazyLock::new(|| {
HashSet::from_iter(vec![
Item::OakSapling,

View file

@ -26,7 +26,7 @@ const SECTION_HEIGHT: u32 = 16;
pub struct PartialChunkStorage {
/// The center of the view, i.e. the chunk the player is currently in.
view_center: ChunkPos,
chunk_radius: u32,
pub(crate) chunk_radius: u32,
view_range: u32,
// chunks is a list of size chunk_radius * chunk_radius
chunks: Box<[Option<Arc<RwLock<Chunk>>>]>,

View file

@ -4,7 +4,7 @@ use std::{
};
use azalea_core::{registry_holder::RegistryHolder, resource_location::ResourceLocation};
use bevy_ecs::{component::Component, system::Resource};
use bevy_ecs::{component::Component, resource::Resource};
use derive_more::{Deref, DerefMut};
use nohash_hasher::IntMap;
use parking_lot::RwLock;
@ -46,7 +46,7 @@ impl InstanceContainer {
/// Add an empty world to the container (unless it already exists) and
/// returns a strong reference to the world.
#[must_use = "the world will be immediately forgotten if unused"]
pub fn insert(
pub fn get_or_insert(
&mut self,
name: ResourceLocation,
height: u32,

View file

@ -41,6 +41,12 @@ impl PartialInstance {
entity_infos: PartialEntityInfos::new(owner_entity),
}
}
/// Clears the internal references to chunks in the PartialInstance and
/// resets the view center.
pub fn reset(&mut self) {
self.chunks = PartialChunkStorage::new(self.chunks.chunk_radius);
}
}
/// An entity ID used by Minecraft.

View file

@ -84,6 +84,8 @@ Azalea lets you create "swarms", which are a group of bots in the same world tha
Azalea uses [Bevy ECS](https://docs.rs/bevy_ecs) internally to store information about the world and clients. Bevy plugins are more powerful than async handler functions, but more difficult to use. See [pathfinder](https://github.com/azalea-rs/azalea/blob/main/azalea/src/pathfinder/mod.rs) as an example of how to make a plugin. You can then enable a plugin by adding `.add_plugin(ExamplePlugin)` in your client/swarm builder.
Everything in Azalea is implemented as a Bevy plugin, which means you can disable default behaviors (like, physics or chat signing) by disabling built-in plugins. See [`SwarmBuilder::new_without_plugins`] to learn how to do that.
Also note that just because something is an entity in the ECS doesn't mean that it's a Minecraft entity. You can filter for that by having `With<MinecraftEntityId>` as a filter.
See the [Bevy Cheatbook](https://bevy-cheatbook.github.io/programming/ecs-intro.html) to learn more about Bevy ECS (and the ECS paradigm in general).

View file

@ -3,7 +3,7 @@ use std::{hint::black_box, sync::Arc, time::Duration};
use azalea::{
BlockPos,
pathfinder::{
astar::{self, PathfinderTimeout, a_star},
astar::{self, PathfinderTimeout, WeightedNode, a_star},
goals::{BlockPosGoal, Goal},
mining::MiningCache,
rel_block_pos::RelBlockPos,
@ -165,6 +165,55 @@ fn bench_pathfinder(c: &mut Criterion) {
run_pathfinder_benchmark(b, generate_mining_world);
});
slow_group.finish();
c.bench_function("weighted_node_le g_score", |b| {
b.iter(|| {
WeightedNode::le(
&black_box(WeightedNode {
index: 0,
g_score: 1.,
f_score: 0.,
}),
&black_box(WeightedNode {
index: 0,
g_score: 0.,
f_score: 0.,
}),
)
});
});
c.bench_function("weighted_node_le f_score", |b| {
b.iter(|| {
WeightedNode::le(
&black_box(WeightedNode {
index: 0,
g_score: 0.,
f_score: 1.,
}),
&black_box(WeightedNode {
index: 0,
g_score: 0.,
f_score: 0.,
}),
)
});
});
c.bench_function("weighted_node_le eq", |b| {
b.iter(|| {
WeightedNode::le(
&black_box(WeightedNode {
index: 0,
g_score: 0.,
f_score: 0.,
}),
&black_box(WeightedNode {
index: 0,
g_score: 0.,
f_score: 0.,
}),
)
});
});
}
criterion_group!(benches, bench_pathfinder);

View file

@ -48,7 +48,7 @@ fn look_at_everything(
look_target.y += **eye_height as f64;
}
look_at_event.send(LookAtEvent {
look_at_event.write(LookAtEvent {
entity: bot_id,
position: look_target,
});

View file

@ -58,11 +58,10 @@ async fn steal(bot: Client, state: State) -> anyhow::Result<()> {
.world()
.read()
.find_blocks(bot.position(), &azalea::registry::Block::Chest.into())
.filter(
// filter for chests that haven't been checked
|block_pos| !state.checked_chests.lock().contains(&block_pos),
)
.next();
.find(
// find the closest chest that hasn't been checked
|block_pos| !state.checked_chests.lock().contains(block_pos),
);
let Some(chest_block) = chest_block else {
break;
};

View file

@ -102,12 +102,12 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
commands.register(literal("lookingat").executes(|ctx: &Ctx| {
let source = ctx.source.lock();
let hit_result = *source.bot.component::<HitResultComponent>();
let hit_result = source.bot.component::<HitResultComponent>();
if hit_result.miss {
let Some(hit_result) = hit_result.as_block_hit_result_if_not_miss() else {
source.reply("I'm not looking at anything");
return 1;
}
};
let block_pos = hit_result.block_pos;
let block = source.bot.world().read().get_block_state(&block_pos);
@ -174,6 +174,13 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
1
}));
commands.register(literal("startuseitem").executes(|ctx: &Ctx| {
let source = ctx.source.lock();
source.bot.start_use_item();
source.reply("Ok!");
1
}));
commands.register(literal("debugecsleak").executes(|ctx: &Ctx| {
let source = ctx.source.lock();
@ -226,7 +233,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
let instance_container = ecs.resource::<InstanceContainer>();
for (instance_name, instance) in &instance_container.instances {
writeln!(report, "- Name: {}", instance_name).unwrap();
writeln!(report, "- Name: {instance_name}").unwrap();
writeln!(report, "- Reference count: {}", instance.strong_count())
.unwrap();
if let Some(instance) = instance.upgrade() {

View file

@ -116,7 +116,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
get_integer(ctx, "y").unwrap(),
get_integer(ctx, "z").unwrap(),
);
println!("{:?}", pos);
println!("{pos:?}");
let source = ctx.source.lock();
source.bot.look_at(pos.center());
1

View file

@ -39,8 +39,8 @@ pub fn tick(bot: Client, state: State) -> anyhow::Result<()> {
}
}
if let Some(nearest_entity) = nearest_entity {
println!("attacking {:?}", nearest_entity);
println!("distance {:?}", nearest_distance);
println!("attacking {nearest_entity:?}");
println!("distance {nearest_distance:?}");
bot.attack(nearest_entity);
}

View file

@ -32,7 +32,7 @@ use std::{sync::Arc, thread};
use azalea::ClientInformation;
use azalea::brigadier::command_dispatcher::CommandDispatcher;
use azalea::ecs::prelude::*;
use azalea::pathfinder::PathfinderDebugParticles;
use azalea::pathfinder::debug::PathfinderDebugParticles;
use azalea::prelude::*;
use azalea::swarm::prelude::*;
use commands::{CommandSource, register_commands};
@ -192,14 +192,10 @@ async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Resu
Ok(())
}
async fn swarm_handle(swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> anyhow::Result<()> {
async fn swarm_handle(_swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> anyhow::Result<()> {
match &event {
SwarmEvent::Disconnect(account, join_opts) => {
SwarmEvent::Disconnect(account, _join_opts) => {
println!("bot got kicked! {}", account.username);
tokio::time::sleep(Duration::from_secs(5)).await;
swarm
.add_and_retry_forever_with_opts(account, State::default(), join_opts)
.await;
}
SwarmEvent::Chat(chat) => {
if chat.message().to_string() == "The particle was not visible for anybody" {
@ -245,7 +241,7 @@ fn parse_args() -> Args {
pathfinder_debug_particles = true;
}
_ => {
eprintln!("Unknown argument: {}", arg);
eprintln!("Unknown argument: {arg}");
process::exit(1);
}
}

View file

@ -26,7 +26,7 @@ fn auto_respawn(
mut perform_respawn_events: EventWriter<PerformRespawnEvent>,
) {
for event in events.read() {
perform_respawn_events.send(PerformRespawnEvent {
perform_respawn_events.write(PerformRespawnEvent {
entity: event.entity,
});
}

View file

@ -95,11 +95,11 @@ pub fn accurate_best_tool_in_hotbar_for_block(
}
}
}
if let Some(this_item_speed) = this_item_speed {
if this_item_speed > best_speed {
best_slot = Some(i);
best_speed = this_item_speed;
}
if let Some(this_item_speed) = this_item_speed
&& this_item_speed > best_speed
{
best_slot = Some(i);
best_speed = this_item_speed;
}
}

View file

@ -1,6 +1,5 @@
use std::f64::consts::PI;
use azalea_client::interact::SwingArmEvent;
use azalea_client::mining::Mining;
use azalea_client::tick_broadcast::{TickBroadcast, UpdateBroadcast};
use azalea_core::position::{BlockPos, Vec3};
@ -11,8 +10,7 @@ use azalea_entity::{
};
use azalea_physics::PhysicsSet;
use bevy_app::Update;
use bevy_ecs::prelude::Event;
use bevy_ecs::schedule::IntoSystemConfigs;
use bevy_ecs::prelude::*;
use futures_lite::Future;
use tracing::trace;
@ -173,10 +171,6 @@ impl BotClientExt for azalea_client::Client {
async fn mine(&self, position: BlockPos) {
self.start_mining(position);
// vanilla sends an extra swing arm packet when we start mining
self.ecs.lock().send_event(SwingArmEvent {
entity: self.entity,
});
let mut receiver = self.get_tick_broadcaster();
while receiver.recv().await.is_ok() {

View file

@ -14,6 +14,7 @@ pub mod prelude;
pub mod swarm;
use std::net::SocketAddr;
use std::time::Duration;
use app::Plugins;
pub use azalea_auth as auth;
@ -43,7 +44,8 @@ use protocol::{ServerAddress, resolver::ResolverError};
use swarm::SwarmBuilder;
use thiserror::Error;
pub type BoxHandleFn<S, R> = Box<dyn Fn(Client, azalea_client::Event, S) -> BoxFuture<'static, R>>;
pub type BoxHandleFn<S, R> =
Box<dyn Fn(Client, azalea_client::Event, S) -> BoxFuture<'static, R> + Send>;
pub type HandleFn<S, Fut> = fn(Client, azalea_client::Event, S) -> Fut;
#[derive(Error, Debug)]
@ -76,6 +78,7 @@ pub struct ClientBuilder<S, R>
where
S: Default + Send + Sync + Clone + Component + 'static,
R: Send + 'static,
Self: Send,
{
/// Internally, ClientBuilder is just a wrapper over SwarmBuilder since it's
/// technically just a subset of it so we can avoid duplicating code this
@ -91,8 +94,10 @@ impl ClientBuilder<NoState, ()> {
.add_plugins(DefaultBotPlugins)
}
/// [`Self::new`] but without adding the plugins by default. This is useful
/// if you want to disable a default plugin.
/// [`Self::new`] but without adding the plugins by default.
///
/// This is useful if you want to disable a default plugin. This also exists
/// for swarms, see [`SwarmBuilder::new_without_plugins`].
///
/// Note that you can also disable `LogPlugin` by disabling the `log`
/// feature.
@ -101,12 +106,11 @@ impl ClientBuilder<NoState, ()> {
///
/// ```
/// # use azalea::prelude::*;
/// use azalea::{app::PluginGroup, DefaultBotPlugins, DefaultPlugins};
/// use bevy_log::LogPlugin;
/// use azalea::app::PluginGroup;
///
/// let client_builder = ClientBuilder::new_without_plugins()
/// .add_plugins(DefaultPlugins.build().disable::<LogPlugin>())
/// .add_plugins(DefaultBotPlugins);
/// .add_plugins(azalea::DefaultPlugins.build().disable::<azalea::chat_signing::ChatSigningPlugin>())
/// .add_plugins(azalea::DefaultBotPlugins);
/// # client_builder.set_handler(handle);
/// # #[derive(Component, Clone, Default)]
/// # pub struct State;
@ -124,7 +128,12 @@ impl ClientBuilder<NoState, ()> {
/// Set the function that's called every time a bot receives an [`Event`].
/// This is the way to handle normal per-bot events.
///
/// Currently you can have up to one client handler.
/// Currently, you can have up to one client handler.
///
/// Note that if you're creating clients directly from the ECS using
/// [`StartJoinServerEvent`] and the client wasn't already in the ECS, then
/// the handler function won't be called for that client. This shouldn't be
/// a concern for most bots, though.
///
/// ```
/// # use azalea::prelude::*;
@ -137,6 +146,8 @@ impl ClientBuilder<NoState, ()> {
/// Ok(())
/// }
/// ```
///
/// [`StartJoinServerEvent`]: azalea_client::join::StartJoinServerEvent
#[must_use]
pub fn set_handler<S, Fut, R>(self, handler: HandleFn<S, Fut>) -> ClientBuilder<S, R>
where
@ -161,12 +172,31 @@ where
self
}
/// Add a group of plugins to the client.
///
/// See [`Self::new_without_plugins`] to learn how to disable default
/// plugins.
#[must_use]
pub fn add_plugins<M>(mut self, plugins: impl Plugins<M>) -> Self {
self.swarm = self.swarm.add_plugins(plugins);
self
}
/// Configures the auto-reconnection behavior for our bot.
///
/// If this is `Some`, then it'll set the default reconnection delay for our
/// bot (how long it'll wait after being kicked before it tries
/// rejoining). if it's `None`, then auto-reconnecting will be disabled.
///
/// If this function isn't called, then our client will reconnect after
/// [`DEFAULT_RECONNECT_DELAY`].
///
/// [`DEFAULT_RECONNECT_DELAY`]: azalea_client::auto_reconnect::DEFAULT_RECONNECT_DELAY
#[must_use]
pub fn reconnect_after(mut self, delay: impl Into<Option<Duration>>) -> Self {
self.swarm.reconnect_after = delay.into();
self
}
/// Build this `ClientBuilder` into an actual [`Client`] and join the given
/// server. If the client can't join, it'll keep retrying forever until it
/// can.

View file

@ -34,7 +34,7 @@ use bevy_ecs::{
/// continue;
/// };
///
/// chat_events.send(SendChatEvent {
/// chat_events.write(SendChatEvent {
/// entity: bot_id,
/// content: String::from("Ahhh!"),
/// });

View file

@ -66,6 +66,7 @@ where
let mut best_path_scores: [f32; 7] = [heuristic(start); 7];
let mut num_nodes = 0;
let mut num_movements = 0;
while let Some(WeightedNode { index, g_score, .. }) = open_set.pop() {
num_nodes += 1;
@ -94,6 +95,7 @@ where
if tentative_g_score - g_score < MIN_IMPROVEMENT {
continue;
}
num_movements += 1;
match nodes.entry(neighbor.movement.target) {
indexmap::map::Entry::Occupied(mut e) => {
@ -166,10 +168,13 @@ where
let best_path = determine_best_path(best_paths, 0);
let elapsed_seconds = start_time.elapsed().as_secs_f64();
let nodes_per_second = (num_nodes as f64 / elapsed_seconds) as u64;
let num_movements_per_second = (num_movements as f64 / elapsed_seconds) as u64;
debug!(
"A* ran at {} nodes per second",
((num_nodes as f64 / start_time.elapsed().as_secs_f64()) as u64)
.to_formatted_string(&num_format::Locale::en)
"A* ran at {} nodes per second and {} movements per second",
nodes_per_second.to_formatted_string(&num_format::Locale::en),
num_movements_per_second.to_formatted_string(&num_format::Locale::en),
);
Path {
@ -262,22 +267,38 @@ impl<P: Hash + Copy + Clone, M: Clone> Clone for Movement<P, M> {
}
#[derive(PartialEq)]
#[repr(C)]
pub struct WeightedNode {
index: usize,
/// The actual cost to get to this node
g_score: f32,
/// Sum of the g_score and heuristic
f_score: f32,
pub f_score: f32,
/// The actual cost to get to this node
pub g_score: f32,
pub index: usize,
}
impl Ord for WeightedNode {
#[inline]
fn cmp(&self, other: &Self) -> cmp::Ordering {
// intentionally inverted to make the BinaryHeap a min-heap
match other.f_score.total_cmp(&self.f_score) {
cmp::Ordering::Equal => self.g_score.total_cmp(&other.g_score),
s => s,
// we compare bits instead of floats because it's faster. this is the same as
// f32::total_cmp as long as the numbers aren't negative
debug_assert!(self.f_score >= 0.0);
debug_assert!(other.f_score >= 0.0);
debug_assert!(self.g_score >= 0.0);
debug_assert!(other.g_score >= 0.0);
let self_f_score = self.f_score.to_bits() as i32;
let other_f_score = other.f_score.to_bits() as i32;
if self_f_score == other_f_score {
let self_g_score = self.g_score.to_bits() as i32;
let other_g_score = other.g_score.to_bits() as i32;
return self_g_score.cmp(&other_g_score);
}
// intentionally inverted to make the BinaryHeap a min-heap
other_f_score.cmp(&self_f_score)
}
}
impl Eq for WeightedNode {}
@ -305,3 +326,37 @@ impl Default for PathfinderTimeout {
Self::Time(Duration::from_secs(1))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn weighted_node(f: f32, g: f32) -> WeightedNode {
WeightedNode {
f_score: f,
g_score: g,
index: 0,
}
}
#[test]
fn test_weighted_node_eq() {
let a = weighted_node(0., 0.);
let b = weighted_node(0., 0.);
assert!(a == b);
}
#[test]
fn test_weighted_node_le() {
let a = weighted_node(1., 0.);
let b = weighted_node(0., 0.);
assert_eq!(a.cmp(&b), cmp::Ordering::Less);
assert!(a.le(&b));
}
#[test]
fn test_weighted_node_le_g() {
let a = weighted_node(0., 1.);
let b = weighted_node(0., 0.);
assert_eq!(a.cmp(&b), cmp::Ordering::Greater);
assert!(!a.le(&b));
}
}

View file

@ -10,7 +10,7 @@ use super::ExecutingPath;
///
/// ```
/// # use azalea::prelude::*;
/// # use azalea::pathfinder::PathfinderDebugParticles;
/// # use azalea::pathfinder::debug::PathfinderDebugParticles;
/// # #[derive(Component, Clone, Default)]
/// # pub struct State;
///

View file

@ -194,7 +194,24 @@ impl<T: Goal> Goal for AndGoals<T> {
#[derive(Clone, Debug)]
pub struct ReachBlockPosGoal {
pub pos: BlockPos,
pub distance: f64,
pub chunk_storage: ChunkStorage,
max_check_distance: i32,
}
impl ReachBlockPosGoal {
pub fn new(pos: BlockPos, chunk_storage: ChunkStorage) -> Self {
Self::new_with_distance(pos, 4.5, chunk_storage)
}
pub fn new_with_distance(pos: BlockPos, distance: f64, chunk_storage: ChunkStorage) -> Self {
Self {
pos,
distance,
chunk_storage,
max_check_distance: (distance + 2.).ceil() as i32,
}
}
}
impl Goal for ReachBlockPosGoal {
fn heuristic(&self, n: BlockPos) -> f32 {
@ -202,21 +219,18 @@ impl Goal for ReachBlockPosGoal {
}
fn success(&self, n: BlockPos) -> bool {
// only do the expensive check if we're close enough
let max_pick_range = 6;
let actual_pick_range = 4.5;
let distance = (self.pos - n).length_squared();
if distance > max_pick_range * max_pick_range {
if distance > self.max_check_distance * self.max_check_distance {
return false;
}
let eye_position = n.to_vec3_floored() + Vec3::new(0.5, 1.62, 0.5);
let look_direction = crate::direction_looking_at(&eye_position, &self.pos.center());
let block_hit_result = azalea_client::interact::pick(
let block_hit_result = azalea_client::interact::pick_block(
&look_direction,
&eye_position,
&self.chunk_storage,
actual_pick_range,
self.distance,
);
block_hit_result.block_pos == self.pos

View file

@ -4,7 +4,7 @@
pub mod astar;
pub mod costs;
mod debug;
pub mod debug;
pub mod goals;
pub mod mining;
pub mod moves;
@ -32,17 +32,15 @@ use azalea_entity::{Physics, Position};
use azalea_physics::PhysicsSet;
use azalea_world::{InstanceContainer, InstanceName};
use bevy_app::{PreUpdate, Update};
use bevy_ecs::prelude::Event;
use bevy_ecs::query::Changed;
use bevy_ecs::schedule::IntoSystemConfigs;
use bevy_ecs::prelude::*;
use bevy_tasks::{AsyncComputeTaskPool, Task};
use futures_lite::future;
use goals::BlockPosGoal;
use parking_lot::RwLock;
use rel_block_pos::RelBlockPos;
use tokio::sync::broadcast::error::RecvError;
use tracing::{debug, error, info, trace, warn};
pub use self::debug::PathfinderDebugParticles;
use self::debug::debug_render_path_with_particles;
use self::goals::Goal;
use self::mining::MiningCache;
@ -253,7 +251,11 @@ impl PathfinderClientExt for azalea_client::Client {
let mut tick_broadcaster = self.get_tick_broadcaster();
while !self.is_goto_target_reached() {
// check every tick
tick_broadcaster.recv().await.unwrap();
match tick_broadcaster.recv().await {
Ok(_) => (),
Err(RecvError::Closed) => return,
Err(err) => warn!("{err}"),
};
}
}
@ -473,7 +475,7 @@ pub fn handle_tasks(
for (entity, mut task) in &mut transform_tasks {
if let Some(optional_path_found_event) = future::block_on(future::poll_once(&mut task.0)) {
if let Some(path_found_event) = optional_path_found_event {
path_found_events.send(path_found_event);
path_found_events.write(path_found_event);
}
// Task is complete, so remove task component from entity
@ -694,7 +696,7 @@ pub fn check_node_reached(
if executing_path.path.is_empty() {
info!("the path we just swapped to was empty, so reached end of path");
walk_events.send(StartWalkEvent {
walk_events.write(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
@ -708,17 +710,17 @@ pub fn check_node_reached(
if executing_path.path.is_empty() {
debug!("pathfinder path is now empty");
walk_events.send(StartWalkEvent {
walk_events.write(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
commands.entity(entity).remove::<ExecutingPath>();
if let Some(goal) = pathfinder.goal.clone() {
if goal.success(movement.target) {
info!("goal was reached!");
pathfinder.goal = None;
pathfinder.successors_fn = None;
}
if let Some(goal) = pathfinder.goal.clone()
&& goal.success(movement.target)
{
info!("goal was reached!");
pathfinder.goal = None;
pathfinder.successors_fn = None;
}
}
@ -873,17 +875,17 @@ fn patch_path(
let mut is_patch_complete = false;
if let Some(path_found_event) = path_found_event {
if let Some(found_path_patch) = path_found_event.path {
if !found_path_patch.is_empty() {
new_path.extend(found_path_patch);
if let Some(found_path_patch) = path_found_event.path
&& !found_path_patch.is_empty()
{
new_path.extend(found_path_patch);
if !path_found_event.is_partial {
new_path.extend(executing_path.path.iter().skip(*patch_nodes.end()).cloned());
is_patch_complete = true;
debug!("the patch is not partial :)");
} else {
debug!("the patch is partial, throwing away rest of path :(");
}
if !path_found_event.is_partial {
new_path.extend(executing_path.path.iter().skip(*patch_nodes.end()).cloned());
is_patch_complete = true;
debug!("the patch is not partial :)");
} else {
debug!("the patch is partial, throwing away rest of path :(");
}
}
} else {
@ -919,7 +921,7 @@ pub fn recalculate_near_end_of_path(
"recalculate_near_end_of_path executing_path.is_path_partial: {}",
executing_path.is_path_partial
);
goto_events.send(GotoEvent {
goto_events.write(GotoEvent {
entity,
goal,
successors_fn,
@ -943,7 +945,7 @@ pub fn recalculate_near_end_of_path(
info!(
"the path we just swapped to was empty, so reached end of path"
);
walk_events.send(StartWalkEvent {
walk_events.write(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
@ -951,7 +953,7 @@ pub fn recalculate_near_end_of_path(
break;
}
} else {
walk_events.send(StartWalkEvent {
walk_events.write(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
@ -962,7 +964,7 @@ pub fn recalculate_near_end_of_path(
_ => {
if executing_path.path.is_empty() {
// idk when this can happen but stop moving just in case
walk_events.send(StartWalkEvent {
walk_events.write(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
@ -1026,19 +1028,20 @@ pub fn recalculate_if_has_goal_but_no_path(
mut goto_events: EventWriter<GotoEvent>,
) {
for (entity, mut pathfinder) in &mut query {
if pathfinder.goal.is_some() && !pathfinder.is_calculating {
if let Some(goal) = pathfinder.goal.as_ref().cloned() {
debug!("Recalculating path because it has a goal but no ExecutingPath");
goto_events.send(GotoEvent {
entity,
goal,
successors_fn: pathfinder.successors_fn.unwrap(),
allow_mining: pathfinder.allow_mining,
min_timeout: pathfinder.min_timeout.expect("min_timeout should be set"),
max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"),
});
pathfinder.is_calculating = true;
}
if pathfinder.goal.is_some()
&& !pathfinder.is_calculating
&& let Some(goal) = pathfinder.goal.as_ref().cloned()
{
debug!("Recalculating path because it has a goal but no ExecutingPath");
goto_events.write(GotoEvent {
entity,
goal,
successors_fn: pathfinder.successors_fn.unwrap(),
allow_mining: pathfinder.allow_mining,
min_timeout: pathfinder.min_timeout.expect("min_timeout should be set"),
max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"),
});
pathfinder.is_calculating = true;
}
}
}
@ -1077,7 +1080,7 @@ pub fn handle_stop_pathfinding_event(
}
if executing_path.path.is_empty() {
walk_events.send(StartWalkEvent {
walk_events.write(StartWalkEvent {
entity: event.entity,
direction: WalkDirection::None,
});
@ -1094,7 +1097,7 @@ pub fn stop_pathfinding_on_instance_change(
if !executing_path.path.is_empty() {
debug!("instance changed, clearing path");
executing_path.path.clear();
stop_pathfinding_events.send(StopPathfindingEvent {
stop_pathfinding_events.write(StopPathfindingEvent {
entity,
force: true,
});

Some files were not shown because too many files have changed in this diff Show more