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:
commit
023260833c
116 changed files with 11810 additions and 8999 deletions
42
CHANGELOG.md
Normal file
42
CHANGELOG.md
Normal 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
541
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
20
Cargo.toml
20
Cargo.toml
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 component’s 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 component’s `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("&", "&")
|
||||
.replace("<", "<")
|
||||
// usually unnecessary but good for compatibility
|
||||
.replace(">", ">")
|
||||
.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)
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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), self’s 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")]
|
||||
|
|
|
@ -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}<b>&<br>{END_SPAN}{AQUA}</b>{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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
136
azalea-client/src/plugins/auto_reconnect.rs
Normal file
136
azalea-client/src/plugins/auto_reconnect.rs
Normal 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,
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
186
azalea-client/src/plugins/chat_signing.rs
Normal file
186
azalea-client/src/plugins/chat_signing.rs
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
266
azalea-client/src/plugins/join.rs
Normal file
266
azalea-client/src/plugins/join.rs
Normal 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()));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
¤t_mining_pos,
|
||||
¤t_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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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 },
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
136
azalea-client/tests/login_to_dimension_with_same_name.rs
Normal file
136
azalea-client/tests/login_to_dimension_with_same_name.rs
Normal 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();
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
block_hit_result::BlockHitResult,
|
||||
direction::{Axis, Direction},
|
||||
hit_result::BlockHitResult,
|
||||
math::EPSILON,
|
||||
position::{BlockPos, Vec3},
|
||||
};
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
68
azalea-core/src/hit_result.rs
Normal file
68
azalea-core/src/hit_result.rs
Normal 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 }
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
@ -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},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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>,
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
use azalea_buf::AzBuf;
|
||||
use azalea_protocol_macros::ClientboundGamePacket;
|
||||
|
||||
#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)]
|
||||
pub struct ClientboundSetDisplayChatPreview {
|
||||
pub enabled: bool,
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ pub struct ServerboundTestInstanceBlockAction {
|
|||
pub enum Action {
|
||||
#[default]
|
||||
Init,
|
||||
Qurey,
|
||||
Query,
|
||||
Set,
|
||||
Reset,
|
||||
Save,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>>>]>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -34,7 +34,7 @@ use bevy_ecs::{
|
|||
/// continue;
|
||||
/// };
|
||||
///
|
||||
/// chat_events.send(SendChatEvent {
|
||||
/// chat_events.write(SendChatEvent {
|
||||
/// entity: bot_id,
|
||||
/// content: String::from("Ahhh!"),
|
||||
/// });
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
///
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue