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

Compare commits

...

150 commits

Author SHA1 Message Date
mat
37b124a2b6 upgrade deps 2025-08-02 08:52:39 +08:00
mat
e7bf124ed5 update changelog 2025-07-24 07:31:23 -03:00
mat
9719d00526 add Client::force_stop_pathfinding 2025-07-24 21:26:49 +11:00
mat
302752860c update movement code for 1.21.5 changes
fixes grim flags
2025-07-24 04:42:52 -05:30
mat
45f89b48e4 1.21.8 2025-07-24 03:54:33 -03:30
mat
6984a2b9e6 remove incorrect optimization attempt in pathfinder 2025-07-24 19:06:01 +12:00
mat
a5c67d2eee add FastFixedBitSet and use it in the pathfinder 2025-07-24 19:06:01 +12:00
mat
004741b781 delete unused module from azalea-chat 2025-07-24 19:06:01 +12:00
mat
63348dbbea clippy: use is_multiple_of 2025-07-24 19:06:01 +12:00
Kumpelinus
df9d776ff8
Add TicksAlive component (#229)
* Add TicksAlive component

* Rename TicksAlive to TicksConnected

* Move component to plugins/tick_counter.rs and add doc comment
2025-07-21 15:28:41 -05:00
mat
ebc2e0c067
1.21.7 (#227)
* 1.21.7-rc1

* 1.21.7

* update Cargo.lock too
2025-06-30 14:14:45 -05:00
mat
a060b73915 fix ClientboundShowDialog in config 2025-06-27 19:43:54 -04:30
mat
5564d475a2 typos 2025-06-27 14:00:26 -09:00
mat
2064450763 outline_shapes patch for pumpkin extractor isn't necessary anymore 2025-06-27 18:56:53 -03:30
mat
7b8dc189f7 let chains are stabilized 2025-06-26 06:48:25 -12:00
mat
537ec510f0 remove some unused system ordering 2025-06-25 20:06:25 -10:00
mat
f12589ab80 fix invalid look directions on teleport 2025-06-25 16:10:15 -13:45
mat
08c409d048 improve packet_order test, add BlockUpdatePlugin, fix packet order for sprinting 2025-06-25 15:14:39 -12:45
mat
f9e4b65713 start adding packet_order test 2025-06-26 10:05:58 +07:00
Yhgd
af1ef93100
fix: parse int lists in TranslatableComponent with field (#225) 2025-06-18 19:25:15 -05:00
mat
844697c04c 1.21.6 fixes 2025-06-18 08:24:34 +07:00
mat
ffbe7a3e42 1.21.6 (#215) 2025-06-17 06:49:07 -12:00
mat
319d144995 take Entity instead of MinecraftEntityId in Client::attack 2025-06-17 09:14:37 +09:30
mat
f82cf7fa85 cleanup 2025-06-16 17:08:09 -06:30
mat
fd9bf16871 implement EntityHitResult 2025-06-16 21:31:04 +00:00
mat
713dae7110 update changelog 2025-06-15 19:52:26 -05:00
mat
8f70f191bb fix unused import warnings when compiling some crates individually 2025-06-16 05:50:06 +05:00
mat
318d95491c Release independent packages
Generated by cargo-workspaces
2025-06-15 19:21:07 -05:00
mat
57bdb88272 use workspace deps everywhere 2025-06-16 01:20:37 +01:00
mat
6bbb3ec5eb Release independent packages
Generated by cargo-workspaces
2025-06-15 19:13:03 -05:00
mat
1fa1520fea put all azalea-* deps in the workspace Cargo.toml 2025-06-16 04:12:22 +04:00
mat
f64229fcc9 add some missing things to the changelog 2025-06-15 22:42:10 +01:00
mat
a06b79a6c1 update deps 2025-06-15 10:09:47 -10:30
mat
1a983beec1 update changelog 2025-06-15 14:45:56 -05:00
mat
5e81d85d7e add note about current_thread to azalea readme 2025-06-14 20:33:22 -10:30
mat
a2606569bb use owned instead of borrowed Vec3 more 2025-06-11 22:58:41 -06:30
mat
89ddd5e85f fix bench 2025-06-11 15:50:45 -12:45
mat
df092f25ec add pathfinder retry_on_no_path and rework GotoEvent constructor 2025-06-11 16:02:08 -09:00
mat
1b348ceeff implement reverting block state predictions on ack 2025-06-11 22:22:26 +00:00
mat
067ec06f26 add BlockPos::distance_to and length 2025-06-11 05:17:06 -13:00
mat
ab05e7bdae add Client::attack_cooldown_remaining_ticks 2025-06-11 02:55:30 -05:30
mat
9b0bd29db4 take BlockPos instead of &BlockPos in all function arguments 2025-06-11 16:55:33 +11:00
mat
2a6ac0764f add mine_with_auto_tool 2025-06-10 19:47:42 -01:00
mat
e4ead93f19 jump if in water while executing pathfinder path 2025-06-09 21:33:21 -03:30
mat
086f979a28 replace Client::get_open_container and view_container_or_inventory with get_inventory 2025-06-09 13:14:20 -11:00
mat
40bcb62a77 update config_fast_builds 2025-06-09 20:33:06 +12:00
mat
4a4de81961 handle relative teleports correctly and fix entity chunk indexing warnings 2025-06-09 17:15:07 +09:00
mat
45d7371274 insert ClientInformation earlier 2025-06-08 17:44:12 -09:00
mat
3087b0c996 add support for panicking on warn/error in simulation tests 2025-06-08 22:46:26 -03:30
mat
338f931c51 wait for block to exist when calling open_container_at 2025-06-05 00:59:14 -04:00
mat
874f051810 remove comment about a part of a test that i decided not to implement 2025-06-04 20:55:57 -07:00
mat
be81877137 fix panic when receiving add_entity and start_configuration in the same update 2025-06-04 21:53:06 -06:00
mat
93a96786a8 handle set_held_slot packet and add more Into BlockStates impls 2025-06-04 10:26:32 +03:30
mat
123c15a293 fix hashset of blockstate into blockstates impl 2025-06-04 12:16:12 +06:00
mat
bbfca34133 copy player part of container_menu to inventory_menu on close 2025-06-03 17:10:34 -13:00
mat
f5f50b85e5 re-enable click prediction and fix related issues 2025-06-04 01:53:24 -03:30
mat
f311ac27d4 send ServerboundPlayerLoaded on join and respawn 2025-06-03 22:01:50 +03:30
mat
415c0d873e fix CubeVoxelShape::find_index 2025-06-02 23:44:49 -09:00
mat
cc3e64a315 fix collisions bugs 2025-06-03 02:41:25 -06:00
mat
61443fa481 fix wrong sequence number being sent 2025-06-02 21:51:08 -09:30
mat
1edb9d3448 add BlockPos::center_bottom 2025-06-02 17:56:16 -08:00
mat
f3a5e91a8c fix issues when pathfinding to non-full blocks and add Client::view_inventory 2025-06-03 09:52:30 +09:30
mat
7517a207db rename the Block trait to BlockTrait to disambiguate with azalea_registry::Block 2025-06-03 06:11:26 +07:00
mat
abf995a702 replace wait_one_tick with wait_ticks and some other api improvements 2025-06-03 03:48:36 +05:00
mat
04dd6dd0a4 fix login_to_dimension_with_same_name test logging error 2025-06-02 13:18:42 -06:30
mat
5c0e5b1eb3 sort entities_by by distance and improve some docs 2025-06-03 07:40:53 +12:00
mat
cee64cece3 actually send Event::ReceiveChunk 2025-06-02 15:55:39 -03:00
mat
c5ddae58a1 improve docs for biome code 2025-06-02 11:51:03 -06:00
mat
2c5f293210 add Event::ReceiveChunk and find_blocks_in_chunk function 2025-06-02 06:55:38 -10:30
mat
5adf67fe04 fix grammar issues in readme 2025-06-01 19:52:51 -12:00
mat
0569862a1b fix issues related to pathfinder mining 2025-06-02 03:14:08 -04:30
mat
d7cd305059 fix VibrationParticle 2025-06-02 10:30:28 +05:00
mat
8da179b221 simplify some join logic so the Entity is returned even on connection error 2025-06-02 04:12:07 -01:00
mat
3d121722d7 several pathfinder fixes 2025-06-02 03:44:24 -01:00
mat
99659bd9a3 add CustomPathfinderState 2025-06-01 21:01:31 -05:45
mat
1d3a7c969f add Client::entities_by and improve some docs 2025-06-01 09:57:54 -12:45
mat
d028d7c3e9 add basic support for getting biome ids in chunks 2025-06-02 07:45:26 +11:00
mat
b103e6fdc0 doc updates 2025-05-31 23:14:59 -06:00
mat
0a7648ce48 add '@generated' to generated code 2025-05-30 18:09:44 -10:00
mat
f27c87b291 more formatting fixes 2025-05-30 19:22:09 -08:45
mat
e37524899e formatting: merge imports 2025-05-30 14:44:48 -13:00
mat
ae4b1e85e6 fix clippy issues and improve formatting everywhere 2025-05-30 19:36:59 -08:00
mat
a64c650504 make fixedbitset require generic const exprs again :3 2025-05-30 12:59:08 -13:45
mat
cfdd8e690f update changelog 2025-05-31 10:47:23 +11:00
mat
a5e7ff771d implement missing brigadier features and cleanup some more 2025-05-30 16:37:40 -07:00
mat
da73b4316d add support for custom suggestions in azalea-brigadier and cleanup a bit 2025-05-28 14:47:18 -09:00
mat
3d340f585a doc fixes 2025-05-23 11:20:57 -13:45
mat
bacb5c0199 patch pathfinder path on cost increase 2025-05-24 01:20:14 +03:00
mat
acd52756be fix Event::Login not firing on reconnect by adding entity id to RemoveOnDisconnectBundle 2025-05-23 12:53:29 -09:00
mat
4bb317a7e1 add line about azalea-auth.json in azalea-auth readme 2025-05-23 19:08:02 -02:00
activepass
9e34a2660d
lowercase named colours (#222) 2025-05-17 01:07:51 -05:00
mat
fbcaa6eca9 rearrange some parts of changelog and mention pr authors 2025-05-10 06:52:26 +03:30
x-osc
b35904a0b5
add distance param for ReachBlockPosGoal (#220)
* add distance param for ReachBlockPosGoal

* add new impl for ReachBlockPosGoal and optimize slightly
2025-05-09 22:12:59 -05:00
mat
6a5a88700c fix offline-mode cert warnings and improve some docs 2025-05-09 15:00:12 -12:00
mat
e1d3b902ba add StartUseItemEvent and improve code related to interactions 2025-05-10 06:22:08 +03:30
mat
e9b3128103 don't send chat signing certs on offline-mode servers 2025-05-09 14:12:51 -10:30
mat
1493c06de5 better docs for disabling plugins 2025-05-07 16:28:03 -05:00
mat
5d799be9db re-export bevy_tasks from azalea_client 2025-05-08 06:09:03 +09:00
mat
ad4eaa174c doc warnings 2025-05-07 15:08:31 -05:45
mat
e0d3352a90 add chat signing 2025-05-08 08:51:34 +12:00
mat
a8e76a0bff impl Display for proxy and trace log JoinOpts 2025-05-07 06:48:59 -11:00
mat
2b7be768f2 reword some things in the changelog 2025-05-06 15:52:16 -13:45
mat
8dff973d26 support legacy hex colors 2025-05-07 14:24:07 +09:00
mat
4a1fdf0121 fix incorrect chat parsing when legacy color codes are mixed in 2025-05-06 22:35:17 -06:30
mat
aa0256da10 upgrade rust version and fix clippy warnings 2025-05-07 11:27:58 +08:00
mat
e9452032bf fix various issues with mining 2025-05-06 14:20:57 -13:00
mat
d58a1c4fa0 delete outdated parts of azalea-protocol readme 2025-05-06 09:09:02 -13:45
mat
685aeff13f fix pathfinder descending onto non-full blocks 2025-05-06 17:51:50 -05:00
mat
7b442368da fix some edge cases when pathfinding on slabs and stairs 2025-05-07 07:09:48 +09:00
mat
f7c9419045 pathfinder can now handle slabs, stairs, and dirt paths 2025-05-07 06:59:22 +10:00
mat
af3affb467 fix chunk errors when joining a world with a same name but different height 2025-05-07 06:00:29 +10:00
mat
68f657310b switch back to stable hickory-resolver 2025-05-06 04:28:23 -13:45
mat
7bbb617bd8 update deps and lock hickory-resolver to a commit 2025-05-03 16:39:56 -06:00
mat
9a40b65bc1
Add AutoReconnectPlugin (#221)
* add AutoReconnectPlugin

* merge main

* start simplifying swarm internals

* fix Swarm::into_iter, handler functions, DisconnectEvent, and add some more docs

* add ClientBuilder/SwarmBuilder::reconnect_after

* fix a doctest

* reword SwarmEvent::Disconnect doc

* better behavior when we try to join twice

* reconnect on ConnectionFailedEvent too

* autoreconnect is less breaking now
2025-05-02 15:55:58 -05:00
mat
52e34de95c typo 2025-05-03 06:52:50 +11:00
mat
50c8e6bc5b the bevy update is a breaking change 2025-05-02 07:52:06 -12:00
mat
5666918519 add changelog 2025-05-03 01:50:30 +06:00
mat
b3f65f9d4b drop dependency on pixlyzer and start using pumpkin extractor 2025-05-01 22:12:00 -08:45
mat
1d3f659c1d make ClientBuilder/SwarmBuilder Send 2025-05-02 02:41:14 +00:00
mat
881055e587 fix InstanceName desync 2025-05-02 01:20:25 +00:00
Kumpelinus
11a74f215e
Implement a to_html method for FormattedText (#208)
* Implement a to_html method for FormattedText

Also fix a small issue with ansi formatting where it duplicated
text.

* cargo fmt

* Make format conversion generic

* cargo lint and fmt

* Fix ascii conversion cleanup

* Implement suggested changes

* format, improve sanitization, add xss test

---------

Co-authored-by: mat <git@matdoes.dev>
2025-05-01 13:26:04 -05:00
mat
4a7d21425c fix some data components 2025-04-25 05:45:55 +03:30
mat
b3af8d73fa update to bevy 0.16 2025-04-25 01:10:03 -01:00
mat
65c9f555b0 unused import 2025-04-24 16:46:46 -09:00
mat
c84b3161ae info log on DisconnectEvent 2025-04-24 17:13:56 -08:00
mat
adef9bf37b always insert Swarm before any systems can start running 2025-04-24 20:55:58 -03:30
mat
89bc5ca91e update RawConnection::state when start_configuration is received 2025-04-24 11:12:47 -12:45
mat
54062c82fd update to git hickory-resolver and use system resolver 2025-04-24 10:03:29 -13:00
mat
0fc0fe41d4 delete some unused packet modules in game state 2025-04-24 18:42:56 -04:00
mat
8012fb90b5 update serialization for set_player_team 2025-04-25 07:24:12 +09:00
mat
c7d53d6532 faster pathfinder WeightedNode::ord 2025-04-22 04:52:02 +03:00
mat
8045b4eda2 add StartJoinServerEvent to allow joining servers exclusively from ecs 2025-04-19 23:51:19 -04:30
mat
ae3722d72c send correct uuid in offline mode 2025-04-19 23:49:11 +05:00
mat
fefc5db09a make azalea::pathfinder::debug public 2025-04-19 21:09:38 +03:00
mat
b7bc08e352 clippy 2025-04-19 23:35:13 +07:00
manen
6c1b144970
remove .unwrap() from wait_until_goto_target_reached (#216)
* fix panics in `wait_until_goto_target_reached`

* replace eprintln with warn

---------

Co-authored-by: mat <git@matdoes.dev>
2025-04-19 11:33:47 -05:00
mat
77f9d929b6 typo in instance_block_action packet 2025-04-18 14:31:18 +08:00
mat
e86087366f box display_name in PlayerInfo to make it smaller 2025-04-18 01:09:49 +02:00
mat
ad45cf5431 allow disabling Event::Packet with a crate feature 2025-04-17 10:15:14 -12:45
mat
43d7c428e3 fix another panic on disconnect and slightly optimize client events loop 2025-04-18 00:32:31 +02:00
mat
3f60bdadac
Move login state to the ECS (#213)
* use packet handlers code for login custom_query

* initial broken implementation for ecs-only login

* fixes

* run Update schedule 60 times per second and delete code related to run_schedule_sender

* fix tests

* fix online-mode

* reply to query packets in a separate system and make it easier for plugins to disable individual replies

* remove unused imports
2025-04-17 16:16:51 -05:00
mat
1989f4ec97 remove unused import 2025-04-17 11:47:24 -09:00
mat
2aa046c4b5 make BlockState::id private 2025-04-17 11:09:14 -09:30
mat
6a83a6fa38 shorten the Plugins bullet point in the readme 2025-04-16 00:01:59 -03:30
mat
b828bc2b12 minor readme fixes 2025-04-15 17:30:56 -10:00
mat
66174fc7d4 clippy and fix broken doc tests 2025-04-16 07:13:09 +04:00
mat
a9820dfd79 make goto async and clean up some examples 2025-04-15 22:04:43 -04:30
mat
1a0c4e2de9 fix wrong MAXIMUM_UNCOMPRESSED_LENGTH value 2025-04-13 16:10:59 +00:00
mat
fd27ca3bec release 0.12.0+mc1.21.5 2025-04-13 05:30:40 +03:00
mat
960b840536 Revert "Release 0.12.0"
This reverts commit e2945b90a9.
2025-04-12 13:33:07 -12:45
322 changed files with 22322 additions and 15612 deletions

View file

@ -1,33 +1,46 @@
# This file was borrowed from Bevy: https://github.com/bevyengine/bevy/blob/main/.cargo/config_fast_builds
# This file is based on Bevy's fast builds config: https://github.com/bevyengine/bevy/blob/main/.cargo/config_fast_builds.toml
# Add the contents of this file to `config.toml` to enable "fast build" configuration. Please read the notes below.
# NOTE: For maximum performance, build using a nightly compiler
# If you are using rust stable, remove the "-Zshare-generics=y" below.
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-Clink-arg=-fuse-ld=lld", "-Zshare-generics=y"]
rustflags = [
# LLD linker
#
# You may need to install it:
#
# - Ubuntu: `sudo apt-get install lld clang`
# - Fedora: `sudo dnf install lld clang`
# - Arch: `sudo pacman -S lld clang`
"-Clink-arg=-fuse-ld=lld",
# Mold linker
#
# You may need to install it:
#
# - Ubuntu: `sudo apt-get install mold clang`
# - Fedora: `sudo dnf install mold clang`
# - Arch: `sudo pacman -S mold clang`
# "-Clink-arg=-fuse-ld=mold",
# Nightly
"-Zshare-generics=y",
]
# NOTE: you must install [Mach-O LLD Port](https://lld.llvm.org/MachO/index.html) on mac. you can easily do this by installing llvm which includes lld with the "brew" package manager:
# `brew install llvm`
[target.x86_64-apple-darwin]
rustflags = [
"-C",
"link-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld",
"-Zshare-generics=y",
]
rustflags = ["-Zshare-generics=y"]
[target.aarch64-apple-darwin]
rustflags = [
"-C",
"link-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld",
"-Zshare-generics=y",
]
rustflags = ["-Zshare-generics=y"]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
rustflags = ["-Zshare-generics=n"]
rustflags = [
# This needs to be off if you use dynamic linking on Windows.
"-Zshare-generics=n",
]
# Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only'
# In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains.

93
CHANGELOG.md Normal file
View file

@ -0,0 +1,93 @@
# 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
- `HitResult` now contains the entity that's being looked at.
- A `QueuedServerBlockUpdates` component that keeps track of block updates per `Update`.
- Local clients now have a `TicksConnected` component. (@Kumpelinus)
### Changed
- Update to Minecraft 1.21.8.
- Renamed `azalea_entity::EntityKind` to `EntityKindComponent` to disambiguate with `azalea_registry::EntityKind`.
- Moved functions and types related to hit results from `azalea::interact` to `azalea::interact::pick`.
- `Client::attack` now takes `Entity` instead of `MinecraftEntityId`.
### Fixed
- Fix packet order for loading (`PlayerLoaded`/`MovePlayerPos`) and sprinting (`PlayerInput`/`PlayerCommand`).
- Clients no longer send invalid look directions if the server teleports us with one.
- Movement code was updated with the changes from 1.21.5, so it no longer flags Grim.
- `azalea-chat` now correctly handles arrays of integers in the `with` field. (@qwqawawow)
## [0.13.0+mc1.21.5] - 2025-06-15
### 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.
- Add `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.
- `ClientBuilder` and `SwarmBuilder` are now Send.
- Add `Client::start_use_item`.
- The pathfinder no longer avoids slabs, stairs, and dirt path blocks.
- The pathfinder now immediately recalculates if blocks are placed in its path.
- Bots that use custom pathfinder moves can now keep arbitrary persistent state by using the `CustomPathfinderState` component and `PathfinderCtx::custom_state`.
- The reach distance for the pathfinder `ReachBlockPosGoal` is now configurable. (@x-osc)
- There is now a `retry_on_no_path` option in `GotoEvent` that can be set to false to make the pathfinder give up if no path could be found.
- azalea-brigadier now supports suggestions, command contexts, result consumers, and returning errors with `ArgumentBuilder::executes_result`.
- Proper support for getting biomes at coordinates.
- Add a new `Client::entities_by` which sorts entities that match a criteria by their distance to the client.
- New client event `Event::ReceiveChunk`.
- Several new functions for interacting with inventories (`Client::get_inventory`, `get_held_item`, `ContainerHandleRef::left_click`, `shift_click`, `right_click`, `slots`).
- Add `Client::mine_with_auto_tool`.
- Add `Client::set_selected_hotbar_slot` and `Client::selected_hotbar_slot`.
- Add `Client::attack_cooldown_remaining_ticks` to complement `has_attack_cooldown`.
- Add `BlockPos::length`, `distance_to`, and `center_bottom`.
### Changed
- `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.
- The `BlockState::id` field is now private, use `.id()` instead.
- Update to [Bevy 0.16](https://bevyengine.org/news/bevy-0-16/).
- Rename `InstanceContainer::insert` to `get_or_insert`.
- Replace `BlockInteractEvent` with the more general-purpose `StartUseItemEvent`.
- Replace `wait_one_tick` and `wait_one_update` with `wait_ticks` and `wait_updates`.
- Functions that took `&Vec3` or `&BlockPos` as arguments now only take them as owned types.
- Rename `azalea_block::Block` to `BlockTrait` to disambiguate with `azalea_registry::Block`.
- `GotoEvent` is now non-enhaustive and should instead be constructed by calling its methods.
### 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.
- 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).
- When patching the path, don't replace the move we're currently executing.
- The correct sequence number is now sent when interacting with blocks.
- Mining is now generally more reliable and doesn't flag Grim.
- Ghost blocks are now handled correctly due to implementing `ClientboundBlockChangedAck`.
- Player eye height was wrong due to being calculated from height instead of being a special case (was 1.53, should've been 1.62).
- The player inventory is now correctly updated when we close a container.
- Inventory interactions are now predicted on the client-side again, and the remaining click operations were implemented.
- `Client::open_container_at` now waits up to 10 ticks for the block to exist if you try to click air.
- Wrong physics collision code resulted in `HitResult` sometimes containing the wrong coordinates and `inside` value.
- Fix the client being unresponsive for a few seconds after joining due to not sending `ServerboundPlayerLoaded`.
- Fix panic when a client received `ClientboundAddEntity` and `ClientboundStartConfiguration` at the same time.
- Fix panic due to `ClientInformation` being inserted too late.
- `ClientboundTeleportEntity` did not handle relative teleports correctly.
- Pathfinder now gets stuck in water less by automatically trying to jump if it's in water.

1391
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -22,30 +22,31 @@ resolver = "2"
# --- Workspace Settings ---
[workspace.package]
version = "0.12.0"
version = "0.13.0+mc1.21.8"
edition = "2024"
license = "MIT"
repository = "https://github.com/azalea-rs/azalea"
# homepage = "https://github.com/azalea-rs/azalea"
[workspace.dependencies]
simdnbt = { version = "0.7", git = "https://github.com/azalea-rs/simdnbt" }
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.1"
bevy_ecs = { version = "0.16.1", default-features = false }
bevy_log = "0.16.1"
bevy_tasks = "0.16.1"
bevy_time = "0.16.1"
byteorder = "1.5.0"
cfb8 = "0.8.1"
chrono = { version = "0.4.40", default-features = false }
criterion = "0.5.1"
chrono = { version = "0.4.41", default-features = false }
criterion = "0.7.0"
derive_more = "2.0.1"
enum-as-inner = "0.6.1"
env_logger = "0.11.8"
flate2 = { version = "1.1.1", features = ["zlib-rs"] }
flate2 = { version = "1.1.2", features = ["zlib-rs"] }
futures = "0.3.31"
futures-lite = "2.6.0"
md-5 = "0.10.6"
@ -53,34 +54,55 @@ minecraft_folder_path = "0.1.2"
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"
parking_lot = "0.12.4"
proc-macro2 = "1.0.95"
quote = "1.0.40"
rand = "0.8.4"
rand = "0.9.2"
regex = "1.11.1"
reqwest = { version = "0.12.15", default-features = false }
rsa = "0.9.8"
reqwest = { version = "0.12.22", default-features = false }
rsa = "0.10.0-rc.3"
rsa_public_encrypt_pkcs1 = "0.4.0"
rustc-hash = "2.1.1"
serde = "1.0.219"
serde_json = "1.0.140"
serde_json = "1.0.142"
sha-1 = "0.10.1"
sha2 = "0.10.8"
simdnbt = "0.7"
socks5-impl = "0.6.2"
syn = "2.0.100"
sha2 = "0.11.0-rc.0"
socks5-impl = "0.7.2"
syn = "2.0.104"
thiserror = "2.0.12"
tokio = "1.44.2"
tokio-util = "0.7.14"
tokio = "1.47.1"
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.17"
num-format = "0.4.4"
indexmap = "2.9.0"
indexmap = "2.10.0"
paste = "1.0.15"
compact_str = "0.9.0"
crc32fast = "1.4.2"
crc32fast = "1.5.0"
async-compat = "0.2.4"
azalea-block-macros = { path = "azalea-block/azalea-block-macros", version = "0.13.0" }
azalea-block = { path = "azalea-block", version = "0.13.0" }
azalea-auth = { path = "azalea-auth", version = "0.13.0" }
azalea-brigadier = { path = "azalea-brigadier", version = "0.13.0" }
azalea-buf-macros = { path = "azalea-buf/azalea-buf-macros", version = "0.13.0" }
azalea-buf = { path = "azalea-buf", version = "0.13.0" }
azalea-chat = { path = "azalea-chat", version = "0.13.0" }
azalea-client = { path = "azalea-client", version = "0.13.0", default-features = false }
azalea-core = { path = "azalea-core", version = "0.13.0" }
azalea-crypto = { path = "azalea-crypto", version = "0.13.0" }
azalea-entity = { path = "azalea-entity", version = "0.13.0" }
azalea-inventory-macros = { path = "azalea-inventory/azalea-inventory-macros", version = "0.13.0" }
azalea-inventory = { path = "azalea-inventory", version = "0.13.0" }
azalea-language = { path = "azalea-language", version = "0.13.0" }
azalea-physics = { path = "azalea-physics", version = "0.13.0" }
azalea-protocol-macros = { path = "azalea-protocol/azalea-protocol-macros", version = "0.13.0" }
azalea-protocol = { path = "azalea-protocol", version = "0.13.0" }
azalea-registry-macros = { path = "azalea-registry/azalea-registry-macros", version = "0.13.0" }
azalea-registry = { path = "azalea-registry", version = "0.13.0" }
azalea-world = { path = "azalea-world", version = "0.13.0" }
# --- Profile Settings ---

View file

@ -10,20 +10,21 @@ A collection of Rust crates for making Minecraft bots, clients, and tools.
<!-- The line below is automatically read and updated by the migrate script, so don't change it manually. -->
_Currently supported Minecraft version: `1.21.5`._
_Currently supported Minecraft version: `1.21.8`._
> [!WARNING]
> Azalea is still unfinished, though most crates are in a useable state
> Azalea is still unfinished, though most crates are in a useable state.
## Features
- [Accurate physics](https://github.com/azalea-rs/azalea/blob/main/azalea-physics/src/lib.rs) (but some features like entity pushing and elytras aren't yet implemented)
- [Accurate physics](https://github.com/azalea-rs/azalea/blob/main/azalea-physics/src/lib.rs) (but some features like entity pushing and elytras aren't implemented yet)
- [Pathfinder](https://azalea.matdoes.dev/azalea/pathfinder/index.html)
- [Swarms](https://azalea.matdoes.dev/azalea/swarm/index.html)
- [Breaking blocks](https://azalea.matdoes.dev/azalea/struct.Client.html#method.mine)
- [Block interactions & building](https://azalea.matdoes.dev/azalea/struct.Client.html#method.block_interact) (this doesn't predict the block interactions/placement on the client yet but it's usually fine)
- [Block interactions & building](https://azalea.matdoes.dev/azalea/struct.Client.html#method.block_interact) (this doesn't predict the block interactions/placement on the client yet, but it's usually fine)
- [Inventories](https://azalea.matdoes.dev/azalea/struct.Client.html#impl-ContainerClientExt-for-Client)
- [Attacking entities](https://azalea.matdoes.dev/azalea/struct.Client.html#method.attack) (but you can't get the entity at the crosshair yet)
- [Attacking entities](https://azalea.matdoes.dev/azalea/struct.Client.html#method.attack)
- [Plugins](#plugins)
## Docs
@ -52,7 +53,7 @@ If you'd like to chat about Azalea, you can join the Matrix space at [#azalea:ma
Here's an incomplete list of bots built using Azalea, primarily intended as a reference in addition to the existing documentation and examples:
- [ShayBox/ShaysBot](https://github.com/ShayBox/ShaysBot) - Pearl statis bot featuring a Discord bot, an HTTP API, and more.
- [ShayBox/ShaysBot](https://github.com/ShayBox/ShaysBot) - Pearl stasis bot featuring a Discord bot, an HTTP API, and more.
- [EnderKill98/statis-bot](https://github.com/EnderKill98/stasis-bot) - This bot can automatically detect thrown pearls and later walk there and pull them for you.
- [as1100k/aether](https://github.com/as1100k/aether) - Collection of Minecraft bots and plugins.
- [mat-1/potato-bot-2](https://github.com/mat-1/potato-bot-2) - Hardened Discord chat bridge created for the LiveOverflow SMP.
@ -62,7 +63,7 @@ You can see more projects built with Azalea in the [GitHub dependency graph](htt
## Plugins
Azalea has support for Bevy plugins, which can significantly alter its functionality. Here's some plugins you may find useful:
Azalea has support for Bevy plugins, which can significantly alter its functionality. Here are some plugins that you may find useful:
- [azalea-rs/azalea-viaversion](https://github.com/azalea-rs/azalea-viaversion) - Multi-version compatibility for your Azalea bots using ViaProxy.
- [azalea-rs/azalea-hax](https://github.com/azalea-rs/azalea-hax) - Anti-knockback.

View file

@ -7,12 +7,15 @@ license.workspace = true
repository.workspace = true
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "0.12.0" }
azalea-crypto = { path = "../azalea-crypto", version = "0.12.0" }
azalea-buf.workspace = true
azalea-crypto.workspace = true
base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
md-5.workspace = true
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
reqwest = { workspace = true, default-features = false, features = [
"json",
"rustls-tls",
] }
rsa.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true

View file

@ -2,6 +2,8 @@
A port of Mojang's Authlib and launcher authentication.
The default location of Azalea's cache is at `~/.minecraft/azalea-auth.json`. You can delete or modify this file if you'd like to associate an email with a different account.
# Examples
```no_run
@ -24,4 +26,4 @@ async fn main() {
}
```
Thanks to [wiki contributors](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Microsoft_Authentication_Scheme), [Overhash](https://gist.github.com/OverHash/a71b32846612ba09d8f79c9d775bfadf), and [prismarine-auth contributors](https://github.com/PrismarineJS/prismarine-auth).
Thanks to [wiki contributors](https://minecraft.wiki/w/Microsoft_authentication), [Overhash](https://gist.github.com/OverHash/a71b32846612ba09d8f79c9d775bfadf), and [prismarine-auth contributors](https://github.com/PrismarineJS/prismarine-auth).

View file

@ -3,13 +3,14 @@
use std::{
collections::HashMap,
path::PathBuf,
time::{Instant, SystemTime, UNIX_EPOCH},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
use thiserror::Error;
use tokio::time::sleep;
use tracing::{error, trace};
use uuid::Uuid;
@ -75,8 +76,9 @@ pub async fn auth(email: &str, opts: AuthOpts<'_>) -> Result<AuthResult, AuthErr
None
};
if cached_account.is_some() && !cached_account.as_ref().unwrap().mca.is_expired() {
let account = cached_account.as_ref().unwrap();
if let Some(account) = &cached_account
&& !account.mca.is_expired()
{
// the minecraft auth data is cached and not expired, so we can just
// use that instead of doing auth all over again :)
@ -129,8 +131,8 @@ pub async fn auth(email: &str, opts: AuthOpts<'_>) -> Result<AuthResult, AuthErr
let profile: ProfileResponse = get_profile(&client, &res.minecraft_access_token).await?;
if let Some(cache_file) = opts.cache_file {
if let Err(e) = cache::set_account_in_cache(
if let Some(cache_file) = opts.cache_file
&& let Err(e) = cache::set_account_in_cache(
&cache_file,
email,
CachedAccount {
@ -142,9 +144,8 @@ pub async fn auth(email: &str, opts: AuthOpts<'_>) -> Result<AuthResult, AuthErr
},
)
.await
{
error!("{}", e);
}
{
error!("{}", e);
}
Ok(AuthResult {
@ -328,7 +329,7 @@ pub async fn get_ms_link_code(
Ok(client
.post("https://login.live.com/oauth20_connect.srf")
.form(&vec![
.form(&[
("scope", scope),
("client_id", client_id),
("response_type", "device_code"),
@ -354,17 +355,17 @@ pub async fn get_ms_auth_token(
CLIENT_ID
};
let login_expires_at = Instant::now() + std::time::Duration::from_secs(res.expires_in);
let login_expires_at = Instant::now() + Duration::from_secs(res.expires_in);
while Instant::now() < login_expires_at {
tokio::time::sleep(std::time::Duration::from_secs(res.interval)).await;
sleep(Duration::from_secs(res.interval)).await;
trace!("Polling to check if user has logged in...");
let res = client
.post(format!(
"https://login.live.com/oauth20_token.srf?client_id={client_id}"
))
.form(&vec![
.form(&[
("client_id", client_id),
("device_code", &res.device_code),
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
@ -375,8 +376,8 @@ pub async fn get_ms_auth_token(
.await;
if let Ok(access_token_response) = res {
trace!("access_token_response: {:?}", access_token_response);
let expires_at = SystemTime::now()
+ std::time::Duration::from_secs(access_token_response.expires_in);
let expires_at =
SystemTime::now() + Duration::from_secs(access_token_response.expires_in);
return Ok(ExpiringValue {
data: access_token_response,
expires_at: expires_at
@ -428,7 +429,7 @@ pub async fn refresh_ms_auth_token(
let access_token_response_text = client
.post("https://login.live.com/oauth20_token.srf")
.form(&vec![
.form(&[
("scope", scope),
("client_id", client_id),
("grant_type", "refresh_token"),
@ -441,8 +442,7 @@ pub async fn refresh_ms_auth_token(
let access_token_response: AccessTokenResponse =
serde_json::from_str(&access_token_response_text)?;
let expires_at =
SystemTime::now() + std::time::Duration::from_secs(access_token_response.expires_in);
let expires_at = SystemTime::now() + Duration::from_secs(access_token_response.expires_in);
Ok(ExpiringValue {
data: access_token_response,
expires_at: expires_at
@ -558,7 +558,7 @@ async fn auth_with_minecraft(
.await?;
trace!("{:?}", res);
let expires_at = SystemTime::now() + std::time::Duration::from_secs(res.expires_in);
let expires_at = SystemTime::now() + Duration::from_secs(res.expires_in);
Ok(ExpiringValue {
data: res,
// to seconds since epoch

View file

@ -1,22 +1,27 @@
//! Cache auth information
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{
io,
path::Path,
time::{SystemTime, UNIX_EPOCH},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::{
fs::{self, File},
io::{AsyncReadExt, AsyncWriteExt},
};
use tracing::{debug, trace};
#[derive(Debug, Error)]
pub enum CacheError {
#[error("Failed to read cache file: {0}")]
Read(std::io::Error),
Read(io::Error),
#[error("Failed to write cache file: {0}")]
Write(std::io::Error),
Write(io::Error),
#[error("Failed to create cache file directory: {0}")]
MkDir(std::io::Error),
MkDir(io::Error),
#[error("Failed to parse cache file: {0}")]
Parse(serde_json::Error),
}
@ -94,7 +99,9 @@ async fn set_entire_cache(cache_file: &Path, cache: Vec<CachedAccount>) -> Resul
"Making cache file parent directory at {}",
cache_file_parent.to_string_lossy()
);
std::fs::create_dir_all(cache_file_parent).map_err(CacheError::MkDir)?;
fs::create_dir_all(cache_file_parent)
.await
.map_err(CacheError::MkDir)?;
}
let mut cache_file = File::create(cache_file).await.map_err(CacheError::Write)?;
let cache = serde_json::to_string_pretty(&cache).map_err(CacheError::Parse)?;

View file

@ -10,6 +10,7 @@ pub struct GameProfile {
pub uuid: Uuid,
/// The username of the player.
pub name: String,
// this is an arc to make GameProfile cheaper to clone when the properties are big
pub properties: Arc<HashMap<String, ProfilePropertyValue>>,
}

View file

@ -7,6 +7,6 @@ license.workspace = true
repository.workspace = true
[dependencies]
azalea-block-macros = { path = "./azalea-block-macros", version = "0.12.0" }
azalea-buf = { path = "../azalea-buf", version = "0.12.0" }
azalea-registry = { path = "../azalea-registry", version = "0.12.0" }
azalea-block-macros.workspace = true
azalea-buf.workspace = true
azalea-registry.workspace = true

View file

@ -23,20 +23,20 @@ let block_state: BlockState = azalea_block::blocks::CobblestoneWall {
let block_state: BlockState = azalea_registry::Block::Jukebox.into();
```
## Block trait
## BlockTrait
The [`Block`] trait represents a type of a block. With the the [`Block`] trait, you can get some extra things like the string block ID and some information about the block's behavior. Also, the structs that implement the trait contain the block attributes as fields so it's more convenient to get them. Note that this is often used as `Box<dyn Block>`.
If for some reason you don't want the `Block` trait, set default-features to false.
The [`BlockTrait`] trait represents a type of a block. With [`BlockTrait`], you can get some extra things like the string block ID and some information about the block's behavior. Also, the structs that implement the trait contain the block attributes as fields so it's more convenient to get them. Note that this is often used as `Box<dyn BlockTrait>`.
If for some reason you don't want `BlockTrait`, set `default-features = false`.
```
# use azalea_block::{Block, BlockState};
# use azalea_block::{BlockTrait, BlockState};
# let block_state = BlockState::from(azalea_registry::Block::Jukebox);
let block = Box::<dyn Block>::from(block_state);
let block = Box::<dyn BlockTrait>::from(block_state);
```
```
# use azalea_block::{Block, BlockState};
# use azalea_block::{BlockTrait, BlockState};
# let block_state: BlockState = azalea_registry::Block::Jukebox.into();
if let Some(jukebox) = Box::<dyn Block>::from(block_state).downcast_ref::<azalea_block::blocks::Jukebox>() {
if let Some(jukebox) = Box::<dyn BlockTrait>::from(block_state).downcast_ref::<azalea_block::blocks::Jukebox>() {
// ...
}
```

View file

@ -2,8 +2,7 @@
mod utils;
use std::collections::HashMap;
use std::fmt::Write;
use std::{collections::HashMap, fmt::Write};
use proc_macro::TokenStream;
use proc_macro2::TokenTree;
@ -552,7 +551,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
from_block_to_state_match_inner.extend(quote! {
#block_struct_name {
#from_block_to_state_combination_match_inner
} => BlockState { id: #state_id },
} => BlockState::new_const(#state_id),
});
if is_default {
@ -626,7 +625,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
azalea_registry::Block::#block_name_pascal_case => Box::new(#block_struct_name::default()),
});
from_registry_block_to_blockstate_match.extend(quote! {
azalea_registry::Block::#block_name_pascal_case => BlockState { id: #default_state_id },
azalea_registry::Block::#block_name_pascal_case => BlockState::new_const(#default_state_id),
});
from_registry_block_to_blockstates_match.extend(quote! {
azalea_registry::Block::#block_name_pascal_case => BlockStates::from(#first_state_id..=#last_state_id),
@ -646,7 +645,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let block_id = block.name.to_string();
let from_block_to_state_match = if block.properties_and_defaults.is_empty() {
quote! { BlockState { id: #first_state_id } }
quote! { BlockState::new_const(#first_state_id) }
} else {
quote! {
match self {
@ -661,7 +660,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
#block_struct_fields
}
impl Block for #block_struct_name {
impl BlockTrait for #block_struct_name {
fn behavior(&self) -> BlockBehavior {
#block_behavior
}
@ -717,10 +716,10 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
// ```
// match state_id {
// // this is just an example of how it might look, these state ids are definitely not correct
// 0|3|6 => Some(Self::Axis::X),
// 1|4|7 => Some(Self::Axis::Y),
// 2|5|8 => Some(Self::Axis::Z),
// _ => None
// 0 | 3 | 6 => Some(Self::Axis::X),
// 1 | 4 | 7 => Some(Self::Axis::Y),
// 2 | 5 | 8 => Some(Self::Axis::Z),
// _ => None,
// }
// ```
let mut property_impls = quote! {};
@ -762,7 +761,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
type Value = #value;
fn try_from_block_state(block_state: BlockState) -> Option<Self::Value> {
match block_state.id {
match block_state.id() {
#enum_inner_generated
_ => None
}
@ -786,16 +785,16 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
#block_structs
impl From<BlockState> for Box<dyn Block> {
impl From<BlockState> for Box<dyn BlockTrait> {
fn from(block_state: BlockState) -> Self {
let b = block_state.id;
let b = block_state.id();
match b {
#from_state_to_block_match
_ => panic!("Invalid block state: {}", b),
}
}
}
impl From<azalea_registry::Block> for Box<dyn Block> {
impl From<azalea_registry::Block> for Box<dyn BlockTrait> {
fn from(block: azalea_registry::Block) -> Self {
match block {
#from_registry_block_to_block_match

View file

@ -5,7 +5,7 @@ use std::{
use azalea_buf::{AzaleaRead, AzaleaReadVar, AzaleaWrite, AzaleaWriteVar, BufReadError};
use crate::Block;
use crate::BlockTrait;
/// The type that's used internally to represent a block state ID.
///
@ -27,7 +27,7 @@ pub type BlockStateIntegerRepr = u16;
pub struct BlockState {
/// The protocol ID for the block state. IDs may change every
/// version, so you shouldn't hard-code them or store them in databases.
pub id: BlockStateIntegerRepr,
id: BlockStateIntegerRepr,
}
impl BlockState {
@ -35,12 +35,21 @@ impl BlockState {
/// 0.
pub const AIR: BlockState = BlockState { id: 0 };
/// Create a new BlockState and panic if the block is not a valid state.
///
/// You should probably use [`BlockState::try_from`] instead.
#[inline]
pub(crate) const fn new_const(id: BlockStateIntegerRepr) -> Self {
assert!(Self::is_valid_state(id));
Self { id }
}
/// Whether the block state is possible to exist in vanilla Minecraft.
///
/// It's equivalent to checking that the state ID is not greater than
/// [`Self::MAX_STATE`].
#[inline]
pub fn is_valid_state(state_id: BlockStateIntegerRepr) -> bool {
pub const fn is_valid_state(state_id: BlockStateIntegerRepr) -> bool {
state_id <= Self::MAX_STATE
}
@ -50,6 +59,13 @@ impl BlockState {
pub fn is_air(&self) -> bool {
self == &Self::AIR
}
/// Returns the protocol ID for the block state. IDs may change every
/// version, so you shouldn't hard-code them or store them in databases.
#[inline]
pub const fn id(&self) -> BlockStateIntegerRepr {
self.id
}
}
impl TryFrom<u32> for BlockState {
@ -78,6 +94,12 @@ impl TryFrom<u16> for BlockState {
}
}
}
impl From<BlockState> for u32 {
/// See [`BlockState::id`].
fn from(value: BlockState) -> Self {
value.id as u32
}
}
impl AzaleaRead for BlockState {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
@ -88,7 +110,7 @@ impl AzaleaRead for BlockState {
}
}
impl AzaleaWrite for BlockState {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
u32::azalea_write_var(&(self.id as u32), buf)
}
}
@ -99,14 +121,14 @@ impl Debug for BlockState {
f,
"BlockState(id: {}, {:?})",
self.id,
Box::<dyn Block>::from(*self)
Box::<dyn BlockTrait>::from(*self)
)
}
}
impl From<BlockState> for azalea_registry::Block {
fn from(value: BlockState) -> Self {
Box::<dyn Block>::from(value).as_registry_block()
Box::<dyn BlockTrait>::from(value).as_registry_block()
}
}
@ -127,11 +149,11 @@ mod tests {
#[test]
fn test_from_blockstate() {
let block: Box<dyn Block> = Box::<dyn Block>::from(BlockState::AIR);
let block: Box<dyn BlockTrait> = Box::<dyn BlockTrait>::from(BlockState::AIR);
assert_eq!(block.id(), "air");
let block: Box<dyn Block> =
Box::<dyn Block>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
let block: Box<dyn BlockTrait> =
Box::<dyn BlockTrait>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
assert_eq!(block.id(), "flowering_azalea");
}

View file

@ -20,7 +20,7 @@ pub struct FluidState {
/// set (see FlowingFluid.getFlowing)
pub falling: bool,
}
#[derive(Default, Clone, Debug, PartialEq, Eq)]
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub enum FluidKind {
#[default]
Empty,

View file

@ -2,7 +2,7 @@ use std::fmt::Debug;
use azalea_block_macros::make_block_states;
use crate::{Block, BlockBehavior, BlockState, BlockStates, Property};
use crate::{BlockBehavior, BlockState, BlockStates, BlockTrait, Property};
make_block_states! {
Properties => {
@ -1561,6 +1561,12 @@ make_block_states! {
_1,
_2,
},
"hydration" => DriedGhastHydration {
_0,
_1,
_2,
_3,
},
"pickles" => SeaPicklePickles {
_1,
_2,
@ -2030,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), {},
@ -2111,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), {
@ -2144,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), {
@ -2201,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), {
@ -2242,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),
@ -2462,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),
@ -2527,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),
},
@ -2588,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),
},
@ -2635,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),
@ -2688,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),
},
@ -2713,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,
@ -2744,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), {
@ -2891,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),
@ -3034,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,
@ -3053,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),
@ -3072,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),
@ -3107,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),
},
@ -3235,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),
@ -3320,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(), {
@ -3507,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,
@ -3753,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),
},
@ -3865,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),
@ -3925,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),
@ -3988,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,
@ -4214,6 +4220,11 @@ make_block_states! {
sniffer_egg => BlockBehavior::new().strength(0.5, 0.5), {
"hatch": SnifferEggHatch::_0,
},
dried_ghast => BlockBehavior::new().force_solid(true), {
"facing": FacingCardinal::North,
"hydration": DriedGhastHydration::_0,
"waterlogged": Waterlogged(false),
},
dead_tube_coral_block => BlockBehavior::new().requires_correct_tool_for_drops().strength(1.5, 6.0).force_solid(true), {},
dead_brain_coral_block => BlockBehavior::new().requires_correct_tool_for_drops().strength(1.5, 6.0).force_solid(true), {},
dead_bubble_coral_block => BlockBehavior::new().requires_correct_tool_for_drops().strength(1.5, 6.0).force_solid(true), {},
@ -4619,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),
},
@ -4802,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,
},
@ -4885,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), {
@ -4989,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), {},
@ -5046,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),
},
@ -5255,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,
@ -5589,20 +5600,20 @@ make_block_states! {
"orientation": Orientation::NorthUp,
"triggered": Triggered(false),
},
trial_spawner => BlockBehavior::new().requires_correct_tool_for_drops().strength(50.0, 50.0), {
trial_spawner => BlockBehavior::new().strength(50.0, 50.0), {
"ominous": Ominous(false),
"trial_spawner_state": TrialSpawnerState::Inactive,
},
vault => BlockBehavior::new(), {
vault => BlockBehavior::new().strength(50.0, 50.0), {
"facing": FacingCardinal::North,
"ominous": Ominous(false),
"vault_state": VaultState::Inactive,
},
heavy_core => BlockBehavior::new(), {
heavy_core => BlockBehavior::new().strength(10.0, 1200.0), {
"waterlogged": Waterlogged(false),
},
pale_moss_block => BlockBehavior::new(), {},
pale_moss_carpet => BlockBehavior::new(), {
pale_moss_block => BlockBehavior::new().strength(0.1, 0.1), {},
pale_moss_carpet => BlockBehavior::new().strength(0.1, 0.1), {
"bottom": Bottom(true),
"east": WallEast::None,
"north": WallNorth::None,

View file

@ -15,7 +15,7 @@ pub use block_state::BlockState;
pub use generated::{blocks, properties};
pub use range::BlockStates;
pub trait Block: Debug + Any {
pub trait BlockTrait: Debug + Any {
fn behavior(&self) -> BlockBehavior;
/// Get the Minecraft ID for this block. For example `stone` or
/// `grass_block`.
@ -27,8 +27,8 @@ pub trait Block: Debug + Any {
/// `azalea_registry::Block` doesn't contain any state data.
fn as_registry_block(&self) -> azalea_registry::Block;
}
impl dyn Block {
pub fn downcast_ref<T: Block>(&self) -> Option<&T> {
impl dyn BlockTrait {
pub fn downcast_ref<T: BlockTrait>(&self) -> Option<&T> {
(self as &dyn Any).downcast_ref::<T>()
}
}

View file

@ -3,6 +3,8 @@ use std::{
ops::{Add, RangeInclusive},
};
use azalea_registry::Block;
use crate::{BlockState, block_state::BlockStateIntegerRepr};
#[derive(Debug, Clone)]
@ -14,7 +16,7 @@ impl From<RangeInclusive<BlockStateIntegerRepr>> for BlockStates {
fn from(range: RangeInclusive<BlockStateIntegerRepr>) -> Self {
let mut set = HashSet::with_capacity((range.end() - range.start() + 1) as usize);
for id in range {
set.insert(BlockState { id });
set.insert(BlockState::try_from(id).unwrap_or_default());
}
Self { set }
}
@ -45,18 +47,33 @@ impl Add for BlockStates {
}
}
impl From<HashSet<azalea_registry::Block>> for BlockStates {
fn from(set: HashSet<azalea_registry::Block>) -> Self {
Self {
set: set.into_iter().map(|b| b.into()).collect(),
}
impl From<HashSet<Block>> for BlockStates {
fn from(set: HashSet<Block>) -> Self {
Self::from(&set)
}
}
impl From<&HashSet<azalea_registry::Block>> for BlockStates {
fn from(set: &HashSet<azalea_registry::Block>) -> Self {
Self {
set: set.iter().map(|&b| b.into()).collect(),
impl From<&HashSet<Block>> for BlockStates {
fn from(set: &HashSet<Block>) -> Self {
let mut block_states = HashSet::with_capacity(set.len());
for &block in set {
block_states.extend(BlockStates::from(block));
}
Self { set: block_states }
}
}
impl<const N: usize> From<[Block; N]> for BlockStates {
fn from(arr: [Block; N]) -> Self {
Self::from(&arr[..])
}
}
impl From<&[Block]> for BlockStates {
fn from(arr: &[Block]) -> Self {
let mut block_states = HashSet::with_capacity(arr.len());
for &block in arr {
block_states.extend(BlockStates::from(block));
}
Self { set: block_states }
}
}

View file

@ -11,8 +11,8 @@ bevy_app.workspace = true
bevy_ecs.workspace = true
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "0.12.0", optional = true }
azalea-chat = { path = "../azalea-chat", version = "0.12.0", optional = true }
azalea-buf = { workspace = true, optional = true }
azalea-chat = { workspace = true, optional = true }
parking_lot.workspace = true
[features]

View file

@ -1,13 +1,13 @@
use std::{any::Any, sync::Arc};
use crate::{
exceptions::CommandSyntaxException,
errors::CommandSyntaxError,
string_reader::StringReader,
suggestion::{Suggestions, SuggestionsBuilder},
};
pub trait ArgumentType {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException>;
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError>;
fn list_suggestions(&self, _builder: SuggestionsBuilder) -> Suggestions {
Suggestions::default()

View file

@ -3,7 +3,7 @@ use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext,
exceptions::CommandSyntaxException,
errors::CommandSyntaxError,
string_reader::StringReader,
suggestion::{Suggestions, SuggestionsBuilder},
};
@ -12,7 +12,7 @@ use crate::{
struct Boolean;
impl ArgumentType for Boolean {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
Ok(Arc::new(reader.read_boolean()?))
}

View file

@ -3,7 +3,7 @@ use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext,
exceptions::{BuiltInExceptions, CommandSyntaxException},
errors::{BuiltInError, CommandSyntaxError},
string_reader::StringReader,
};
@ -14,28 +14,28 @@ struct Double {
}
impl ArgumentType for Double {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
let start = reader.cursor;
let result = reader.read_double()?;
if let Some(minimum) = self.minimum {
if result < minimum {
reader.cursor = start;
return Err(BuiltInExceptions::DoubleTooSmall {
found: result,
min: minimum,
}
.create_with_context(reader));
if let Some(minimum) = self.minimum
&& result < minimum
{
reader.cursor = start;
return Err(BuiltInError::DoubleTooSmall {
found: result,
min: minimum,
}
.create_with_context(reader));
}
if let Some(maximum) = self.maximum {
if result > maximum {
reader.cursor = start;
return Err(BuiltInExceptions::DoubleTooBig {
found: result,
max: maximum,
}
.create_with_context(reader));
if let Some(maximum) = self.maximum
&& result > maximum
{
reader.cursor = start;
return Err(BuiltInError::DoubleTooBig {
found: result,
max: maximum,
}
.create_with_context(reader));
}
Ok(Arc::new(result))
}

View file

@ -3,7 +3,7 @@ use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext,
exceptions::{BuiltInExceptions, CommandSyntaxException},
errors::{BuiltInError, CommandSyntaxError},
string_reader::StringReader,
};
@ -14,28 +14,28 @@ struct Float {
}
impl ArgumentType for Float {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
let start = reader.cursor;
let result = reader.read_float()?;
if let Some(minimum) = self.minimum {
if result < minimum {
reader.cursor = start;
return Err(BuiltInExceptions::FloatTooSmall {
found: result,
min: minimum,
}
.create_with_context(reader));
if let Some(minimum) = self.minimum
&& result < minimum
{
reader.cursor = start;
return Err(BuiltInError::FloatTooSmall {
found: result,
min: minimum,
}
.create_with_context(reader));
}
if let Some(maximum) = self.maximum {
if result > maximum {
reader.cursor = start;
return Err(BuiltInExceptions::FloatTooBig {
found: result,
max: maximum,
}
.create_with_context(reader));
if let Some(maximum) = self.maximum
&& result > maximum
{
reader.cursor = start;
return Err(BuiltInError::FloatTooBig {
found: result,
max: maximum,
}
.create_with_context(reader));
}
Ok(Arc::new(result))
}

View file

@ -3,7 +3,7 @@ use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext,
exceptions::{BuiltInExceptions, CommandSyntaxException},
errors::{BuiltInError, CommandSyntaxError},
string_reader::StringReader,
};
@ -14,28 +14,28 @@ struct Integer {
}
impl ArgumentType for Integer {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
let start = reader.cursor;
let result = reader.read_int()?;
if let Some(minimum) = self.minimum {
if result < minimum {
reader.cursor = start;
return Err(BuiltInExceptions::IntegerTooSmall {
found: result,
min: minimum,
}
.create_with_context(reader));
if let Some(minimum) = self.minimum
&& result < minimum
{
reader.cursor = start;
return Err(BuiltInError::IntegerTooSmall {
found: result,
min: minimum,
}
.create_with_context(reader));
}
if let Some(maximum) = self.maximum {
if result > maximum {
reader.cursor = start;
return Err(BuiltInExceptions::IntegerTooBig {
found: result,
max: maximum,
}
.create_with_context(reader));
if let Some(maximum) = self.maximum
&& result > maximum
{
reader.cursor = start;
return Err(BuiltInError::IntegerTooBig {
found: result,
max: maximum,
}
.create_with_context(reader));
}
Ok(Arc::new(result))
}

View file

@ -3,7 +3,7 @@ use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext,
exceptions::{BuiltInExceptions, CommandSyntaxException},
errors::{BuiltInError, CommandSyntaxError},
string_reader::StringReader,
};
@ -14,28 +14,28 @@ struct Long {
}
impl ArgumentType for Long {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
let start = reader.cursor;
let result = reader.read_long()?;
if let Some(minimum) = self.minimum {
if result < minimum {
reader.cursor = start;
return Err(BuiltInExceptions::LongTooSmall {
found: result,
min: minimum,
}
.create_with_context(reader));
if let Some(minimum) = self.minimum
&& result < minimum
{
reader.cursor = start;
return Err(BuiltInError::LongTooSmall {
found: result,
min: minimum,
}
.create_with_context(reader));
}
if let Some(maximum) = self.maximum {
if result > maximum {
reader.cursor = start;
return Err(BuiltInExceptions::LongTooBig {
found: result,
max: maximum,
}
.create_with_context(reader));
if let Some(maximum) = self.maximum
&& result > maximum
{
reader.cursor = start;
return Err(BuiltInError::LongTooBig {
found: result,
max: maximum,
}
.create_with_context(reader));
}
Ok(Arc::new(result))
}

View file

@ -1,9 +1,7 @@
use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext, exceptions::CommandSyntaxException, string_reader::StringReader,
};
use crate::{context::CommandContext, errors::CommandSyntaxError, string_reader::StringReader};
pub enum StringArgument {
/// Match up until the next space.
@ -16,7 +14,7 @@ pub enum StringArgument {
}
impl ArgumentType for StringArgument {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
let result = match self {
StringArgument::SingleWord => reader.read_unquoted_string().to_string(),
StringArgument::QuotablePhrase => reader.read_string()?,

View file

@ -1,18 +1,32 @@
use std::{fmt::Debug, sync::Arc};
use std::{
fmt::{self, Debug},
sync::Arc,
};
use parking_lot::RwLock;
use super::{literal_argument_builder::Literal, required_argument_builder::Argument};
use crate::{
context::CommandContext,
errors::CommandSyntaxError,
modifier::RedirectModifier,
tree::{Command, CommandNode},
};
#[derive(Debug, Clone)]
pub enum ArgumentBuilderType {
#[derive(Debug)]
pub enum ArgumentBuilderType<S> {
Literal(Literal),
Argument(Argument),
Argument(Argument<S>),
}
impl<S> Clone for ArgumentBuilderType<S> {
fn clone(&self) -> Self {
match self {
ArgumentBuilderType::Literal(literal) => ArgumentBuilderType::Literal(literal.clone()),
ArgumentBuilderType::Argument(argument) => {
ArgumentBuilderType::Argument(argument.clone())
}
}
}
}
/// A node that hasn't yet been built.
@ -30,7 +44,7 @@ pub struct ArgumentBuilder<S> {
/// A node that isn't yet built.
impl<S> ArgumentBuilder<S> {
pub fn new(value: ArgumentBuilderType) -> Self {
pub fn new(value: ArgumentBuilderType<S>) -> Self {
Self {
arguments: CommandNode {
value,
@ -49,9 +63,7 @@ impl<S> ArgumentBuilder<S> {
/// ```
/// # use azalea_brigadier::prelude::*;
/// # let mut subject = CommandDispatcher::<()>::new();
/// literal("foo").then(
/// literal("bar").executes(|ctx: &CommandContext<()>| 42)
/// )
/// literal("foo").then(literal("bar").executes(|ctx: &CommandContext<()>| 42))
/// # ;
/// ```
pub fn then(self, argument: ArgumentBuilder<S>) -> Self {
@ -79,6 +91,16 @@ impl<S> ArgumentBuilder<S> {
pub fn executes<F>(mut self, f: F) -> Self
where
F: Fn(&CommandContext<S>) -> i32 + Send + Sync + 'static,
{
self.command = Some(Arc::new(move |ctx: &CommandContext<S>| Ok(f(ctx))));
self
}
/// Same as [`Self::executes`] but returns a `Result<i32,
/// CommandSyntaxError>`.
pub fn executes_result<F>(mut self, f: F) -> Self
where
F: Fn(&CommandContext<S>) -> Result<i32, CommandSyntaxError> + Send + Sync + 'static,
{
self.command = Some(Arc::new(f));
self
@ -163,7 +185,7 @@ impl<S> ArgumentBuilder<S> {
}
impl<S> Debug for ArgumentBuilder<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ArgumentBuilder")
.field("arguments", &self.arguments)
// .field("command", &self.command)

View file

@ -12,7 +12,7 @@ impl Literal {
}
}
impl From<Literal> for ArgumentBuilderType {
impl<S> From<Literal> for ArgumentBuilderType<S> {
fn from(literal: Literal) -> Self {
Self::Literal(literal)
}

View file

@ -1,37 +1,52 @@
use std::{any::Any, fmt::Debug, sync::Arc};
use std::{
any::Any,
fmt::{self, Debug},
sync::Arc,
};
use super::argument_builder::{ArgumentBuilder, ArgumentBuilderType};
use crate::{
arguments::ArgumentType,
exceptions::CommandSyntaxException,
context::CommandContext,
errors::CommandSyntaxError,
string_reader::StringReader,
suggestion::{Suggestions, SuggestionsBuilder},
suggestion::{SuggestionProvider, Suggestions, SuggestionsBuilder},
};
/// An argument node type. The `T` type parameter is the type of the argument,
/// which can be anything.
#[derive(Clone)]
pub struct Argument {
pub struct Argument<S> {
pub name: String,
parser: Arc<dyn ArgumentType + Send + Sync>,
custom_suggestions: Option<Arc<dyn SuggestionProvider<S> + Send + Sync>>,
}
impl Argument {
pub fn new(name: &str, parser: Arc<dyn ArgumentType + Send + Sync>) -> Self {
impl<S> Argument<S> {
pub fn new(
name: &str,
parser: Arc<dyn ArgumentType + Send + Sync>,
custom_suggestions: Option<Arc<dyn SuggestionProvider<S> + Send + Sync>>,
) -> Self {
Self {
name: name.to_string(),
parser,
custom_suggestions,
}
}
pub fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
pub fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
self.parser.parse(reader)
}
pub fn list_suggestions(&self, builder: SuggestionsBuilder) -> Suggestions {
// TODO: custom suggestions
// https://github.com/Mojang/brigadier/blob/master/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java#L71
self.parser.list_suggestions(builder)
pub fn list_suggestions(
&self,
context: CommandContext<S>,
builder: SuggestionsBuilder,
) -> Suggestions {
if let Some(s) = &self.custom_suggestions {
s.get_suggestions(context, builder)
} else {
self.parser.list_suggestions(builder)
}
}
pub fn examples(&self) -> Vec<String> {
@ -39,14 +54,14 @@ impl Argument {
}
}
impl From<Argument> for ArgumentBuilderType {
fn from(argument: Argument) -> Self {
impl<S> From<Argument<S>> for ArgumentBuilderType<S> {
fn from(argument: Argument<S>) -> Self {
Self::Argument(argument)
}
}
impl Debug for Argument {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl<S> Debug for Argument<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Argument")
.field("name", &self.name)
// .field("parser", &self.parser)
@ -59,5 +74,15 @@ pub fn argument<S>(
name: &str,
parser: impl ArgumentType + Send + Sync + 'static,
) -> ArgumentBuilder<S> {
ArgumentBuilder::new(Argument::new(name, Arc::new(parser)).into())
ArgumentBuilder::new(Argument::new(name, Arc::new(parser), None).into())
}
impl<S> Clone for Argument<S> {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
parser: self.parser.clone(),
custom_suggestions: self.custom_suggestions.clone(),
}
}
}

View file

@ -1,7 +1,7 @@
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
mem,
ptr,
rc::Rc,
sync::Arc,
};
@ -10,9 +10,10 @@ use parking_lot::RwLock;
use crate::{
builder::argument_builder::ArgumentBuilder,
context::{CommandContext, CommandContextBuilder},
exceptions::{BuiltInExceptions, CommandSyntaxException},
context::{CommandContextBuilder, ContextChain},
errors::{BuiltInError, CommandSyntaxError},
parse_results::ParseResults,
result_consumer::{DefaultResultConsumer, ResultConsumer},
string_reader::StringReader,
suggestion::{Suggestions, SuggestionsBuilder},
tree::CommandNode,
@ -30,12 +31,14 @@ where
Self: Sync + Send,
{
pub root: Arc<RwLock<CommandNode<S>>>,
consumer: Box<dyn ResultConsumer<S> + Send + Sync>,
}
impl<S> CommandDispatcher<S> {
pub fn new() -> Self {
Self {
root: Arc::new(RwLock::new(CommandNode::default())),
consumer: Box::new(DefaultResultConsumer),
}
}
@ -52,7 +55,7 @@ impl<S> CommandDispatcher<S> {
build
}
pub fn parse(&self, command: StringReader, source: S) -> ParseResults<S> {
pub fn parse(&self, command: StringReader, source: S) -> ParseResults<'_, S> {
let source = Arc::new(source);
let context = CommandContextBuilder::new(self, source, self.root.clone(), command.cursor());
@ -64,10 +67,10 @@ impl<S> CommandDispatcher<S> {
node: &Arc<RwLock<CommandNode<S>>>,
original_reader: &StringReader,
context_so_far: CommandContextBuilder<'a, S>,
) -> Result<ParseResults<'a, S>, CommandSyntaxException> {
) -> Result<ParseResults<'a, S>, CommandSyntaxError> {
let source = context_so_far.source.clone();
#[allow(clippy::mutable_key_type)] // this is fine because we don't mutate the key
let mut errors = HashMap::<Rc<CommandNode<S>>, CommandSyntaxException>::new();
let mut errors = HashMap::<Rc<CommandNode<S>>, CommandSyntaxError>::new();
let mut potentials: Vec<ParseResults<S>> = vec![];
let cursor = original_reader.cursor();
@ -83,7 +86,7 @@ impl<S> CommandDispatcher<S> {
if let Err(ex) = parse_with_context_result {
errors.insert(
Rc::new((*child.read()).clone()),
BuiltInExceptions::DispatcherParseException {
BuiltInError::DispatcherParseException {
message: ex.message(),
}
.create_with_context(&reader),
@ -94,8 +97,7 @@ impl<S> CommandDispatcher<S> {
if reader.can_read() && reader.peek() != ' ' {
errors.insert(
Rc::new((*child.read()).clone()),
BuiltInExceptions::DispatcherExpectedArgumentSeparator
.create_with_context(&reader),
BuiltInError::DispatcherExpectedArgumentSeparator.create_with_context(&reader),
);
reader.cursor = cursor;
continue;
@ -179,11 +181,11 @@ impl<S> CommandDispatcher<S> {
&self,
input: impl Into<StringReader>,
source: S,
) -> Result<i32, CommandSyntaxException> {
) -> Result<i32, CommandSyntaxError> {
let input = input.into();
let parse = self.parse(input, source);
Self::execute_parsed(parse)
self.execute_parsed(parse)
}
pub fn add_paths(
@ -235,91 +237,26 @@ impl<S> CommandDispatcher<S> {
}
/// Executes a given pre-parsed command.
pub fn execute_parsed(parse: ParseResults<S>) -> Result<i32, CommandSyntaxException> {
pub fn execute_parsed(&self, parse: ParseResults<S>) -> Result<i32, CommandSyntaxError> {
if parse.reader.can_read() {
if parse.exceptions.len() == 1 {
return Err(parse.exceptions.values().next().unwrap().clone());
}
if parse.context.range.is_empty() {
return Err(
BuiltInExceptions::DispatcherUnknownCommand.create_with_context(&parse.reader)
);
}
return Err(
BuiltInExceptions::DispatcherUnknownArgument.create_with_context(&parse.reader)
);
return Err(if parse.exceptions.len() == 1 {
parse.exceptions.values().next().unwrap().clone()
} else if parse.context.range.is_empty() {
BuiltInError::DispatcherUnknownCommand.create_with_context(&parse.reader)
} else {
BuiltInError::DispatcherUnknownArgument.create_with_context(&parse.reader)
});
}
let mut result = 0i32;
let mut successful_forks = 0;
let mut forked = false;
let mut found_command = false;
let command = parse.reader.string();
let original = parse.context.build(command);
let mut contexts = vec![original];
let mut next: Vec<CommandContext<S>> = vec![];
let original = Rc::new(parse.context.build(command));
let flat_context = ContextChain::try_flatten(original.clone());
let Some(flat_context) = flat_context else {
self.consumer.on_command_complete(original, false, 0);
return Err(BuiltInError::DispatcherUnknownCommand.create_with_context(&parse.reader));
};
while !contexts.is_empty() {
for context in &contexts {
let child = &context.child;
if let Some(child) = child {
forked |= child.forks;
if child.has_nodes() {
found_command = true;
let modifier = &context.modifier;
if let Some(modifier) = modifier {
let results = modifier(context);
match results {
Ok(results) => {
if !results.is_empty() {
next.extend(
results.iter().map(|s| child.copy_for(s.clone())),
);
}
}
_ => {
// TODO
// self.consumer.on_command_complete(context, false, 0);
if !forked {
return Err(results.err().unwrap());
}
}
}
} else {
next.push(child.copy_for(context.source.clone()));
}
}
} else if let Some(context_command) = &context.command {
found_command = true;
let value = context_command(context);
result += value;
// consumer.on_command_complete(context, true, value);
successful_forks += 1;
// TODO: allow context_command to error and handle
// those errors
}
}
// move next into contexts and clear next
mem::swap(&mut contexts, &mut next);
next.clear();
}
if !found_command {
// consumer.on_command_complete(original, false, 0);
return Err(
BuiltInExceptions::DispatcherUnknownCommand.create_with_context(&parse.reader)
);
}
// TODO: this is not how vanilla does it but it works
Ok(if successful_forks >= 2 {
successful_forks
} else {
result
})
// Ok(if forked { successful_forks } else { result })
flat_context.execute_all(original.source.clone(), self.consumer.as_ref())
}
pub fn get_all_usage(
@ -349,7 +286,7 @@ impl<S> CommandDispatcher<S> {
}
match &node.redirect {
Some(redirect) => {
let redirect = if redirect.data_ptr() == self.root.data_ptr() {
let redirect = if ptr::eq(redirect.data_ptr(), self.root.data_ptr()) {
"...".to_string()
} else {
format!("-> {}", redirect.read().usage_text())
@ -427,7 +364,7 @@ impl<S> CommandDispatcher<S> {
}
if let Some(redirect) = &node.redirect {
let redirect = if redirect.data_ptr() == self.root.data_ptr() {
let redirect = if ptr::eq(redirect.data_ptr(), self.root.data_ptr()) {
"...".to_string()
} else {
format!("-> {}", redirect.read().usage_text())
@ -467,7 +404,7 @@ impl<S> CommandDispatcher<S> {
Ordering::Equal => {
let usage = child_usage.into_iter().next().unwrap();
let usage = if child_optional {
format!("[{}]", usage)
format!("[{usage}]")
} else {
usage
};

View file

@ -1,4 +1,10 @@
use std::{any::Any, collections::HashMap, fmt::Debug, rc::Rc, sync::Arc};
use std::{
any::Any,
collections::HashMap,
fmt::{self, Debug},
rc::Rc,
sync::Arc,
};
use parking_lot::RwLock;
@ -11,15 +17,15 @@ use crate::{
/// A built `CommandContextBuilder`.
pub struct CommandContext<S> {
pub source: Arc<S>,
pub input: String,
pub arguments: HashMap<String, ParsedArgument>,
pub command: Command<S>,
pub root_node: Arc<RwLock<CommandNode<S>>>,
pub nodes: Vec<ParsedCommandNode<S>>,
pub range: StringRange,
pub child: Option<Rc<CommandContext<S>>>,
pub modifier: Option<Arc<RedirectModifier<S>>>,
pub forks: bool,
pub(super) input: String,
pub(super) arguments: HashMap<String, ParsedArgument>,
pub(super) command: Command<S>,
pub(super) root_node: Arc<RwLock<CommandNode<S>>>,
pub(super) nodes: Vec<ParsedCommandNode<S>>,
pub(super) range: StringRange,
pub(super) child: Option<Rc<CommandContext<S>>>,
pub(super) modifier: Option<Arc<RedirectModifier<S>>>,
pub(super) forks: bool,
}
impl<S> Clone for CommandContext<S> {
@ -40,7 +46,7 @@ impl<S> Clone for CommandContext<S> {
}
impl<S> Debug for CommandContext<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CommandContext")
// .field("source", &self.source)
.field("input", &self.input)
@ -59,8 +65,10 @@ impl<S> Debug for CommandContext<S> {
impl<S> CommandContext<S> {
pub fn copy_for(&self, source: Arc<S>) -> Self {
if Arc::ptr_eq(&source, &self.source) {
// fast path
return self.clone();
}
CommandContext {
source,
input: self.input.clone(),
@ -75,12 +83,52 @@ impl<S> CommandContext<S> {
}
}
pub fn child(&self) -> Option<&CommandContext<S>> {
self.child.as_ref().map(|c| c.as_ref())
}
pub fn last_child(&self) -> &CommandContext<S> {
let mut result = self;
while let Some(child) = result.child() {
result = child;
}
result
}
pub fn command(&self) -> &Command<S> {
&self.command
}
pub fn argument(&self, name: &str) -> Option<&dyn Any> {
let argument = self.arguments.get(name);
argument.map(|a| a.result.as_ref())
}
pub fn redirect_modifier(&self) -> Option<&RedirectModifier<S>> {
self.modifier.as_ref().map(|m| m.as_ref())
}
pub fn range(&self) -> &StringRange {
&self.range
}
pub fn input(&self) -> &str {
&self.input
}
pub fn root_node(&self) -> &Arc<RwLock<CommandNode<S>>> {
&self.root_node
}
pub fn nodes(&self) -> &[ParsedCommandNode<S>] {
&self.nodes
}
pub fn has_nodes(&self) -> bool {
!self.nodes.is_empty()
}
pub fn argument(&self, name: &str) -> Option<Arc<dyn Any>> {
let argument = self.arguments.get(name);
argument.map(|a| a.result.clone())
pub fn is_forked(&self) -> bool {
self.forks
}
}

View file

@ -1,4 +1,9 @@
use std::{collections::HashMap, fmt::Debug, rc::Rc, sync::Arc};
use std::{
collections::HashMap,
fmt::{self, Debug},
rc::Rc,
sync::Arc,
};
use parking_lot::RwLock;
@ -140,7 +145,7 @@ impl<'a, S> CommandContextBuilder<'a, S> {
}
impl<S> Debug for CommandContextBuilder<'_, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CommandContextBuilder")
// .field("arguments", &self.arguments)
.field("root", &self.root)

View file

@ -0,0 +1,167 @@
use std::{rc::Rc, sync::Arc};
use super::CommandContext;
use crate::{errors::CommandSyntaxError, result_consumer::ResultConsumer};
pub struct ContextChain<S> {
modifiers: Vec<Rc<CommandContext<S>>>,
executable: Rc<CommandContext<S>>,
next_stage_cache: Option<Rc<ContextChain<S>>>,
}
impl<S> ContextChain<S> {
pub fn new(modifiers: Vec<Rc<CommandContext<S>>>, executable: Rc<CommandContext<S>>) -> Self {
if executable.command.is_none() {
panic!("Last command in chain must be executable");
}
Self {
modifiers,
executable,
next_stage_cache: None,
}
}
pub fn try_flatten(root_context: Rc<CommandContext<S>>) -> Option<Self> {
let mut modifiers = Vec::new();
let mut current = root_context;
loop {
let child = current.child.clone();
let Some(child) = child else {
// Last entry must be executable command
current.command.as_ref()?;
return Some(ContextChain::new(modifiers, current));
};
modifiers.push(current);
current = child;
}
}
pub fn run_modifier(
modifier: Rc<CommandContext<S>>,
source: Arc<S>,
result_consumer: &dyn ResultConsumer<S>,
forked_mode: bool,
) -> Result<Vec<Arc<S>>, CommandSyntaxError> {
let source_modifier = modifier.redirect_modifier();
let Some(source_modifier) = source_modifier else {
return Ok(vec![source]);
};
let context_to_use = Rc::new(modifier.copy_for(source));
let err = match (source_modifier)(&context_to_use) {
Ok(res) => return Ok(res),
Err(e) => e,
};
result_consumer.on_command_complete(context_to_use, false, 0);
if forked_mode {
return Ok(vec![]);
}
Err(err)
}
pub fn run_executable(
&self,
executable: Rc<CommandContext<S>>,
source: Arc<S>,
result_consumer: &dyn ResultConsumer<S>,
forked_mode: bool,
) -> Result<i32, CommandSyntaxError> {
let context_to_use = Rc::new(executable.copy_for(source));
let Some(command) = &executable.command else {
unimplemented!();
};
let err = match (command)(&context_to_use) {
Ok(result) => {
result_consumer.on_command_complete(context_to_use, true, result);
return if forked_mode { Ok(1) } else { Ok(result) };
}
Err(err) => err,
};
result_consumer.on_command_complete(context_to_use, false, 0);
if forked_mode { Ok(0) } else { Err(err) }
}
pub fn execute_all(
&self,
source: Arc<S>,
result_consumer: &dyn ResultConsumer<S>,
) -> Result<i32, CommandSyntaxError> {
if self.modifiers.is_empty() {
return self.run_executable(self.executable.clone(), source, result_consumer, false);
}
let mut forked_mode = false;
let mut current_sources = vec![source];
for modifier in &self.modifiers {
forked_mode |= modifier.is_forked();
let mut next_sources = Vec::new();
for source_to_run in current_sources {
next_sources.extend(Self::run_modifier(
modifier.clone(),
source_to_run.clone(),
result_consumer,
forked_mode,
)?);
}
if next_sources.is_empty() {
return Ok(0);
}
current_sources = next_sources;
}
let mut result = 0;
for execution_source in current_sources {
result += self.run_executable(
self.executable.clone(),
execution_source,
result_consumer,
forked_mode,
)?;
}
Ok(result)
}
pub fn stage(&self) -> Stage {
if self.modifiers.is_empty() {
Stage::Execute
} else {
Stage::Modify
}
}
pub fn top_context(&self) -> Rc<CommandContext<S>> {
self.modifiers
.first()
.cloned()
.unwrap_or_else(|| self.executable.clone())
}
pub fn next_stage(&mut self) -> Option<Rc<ContextChain<S>>> {
let modifier_count = self.modifiers.len();
if modifier_count == 0 {
return None;
}
if self.next_stage_cache.is_none() {
self.next_stage_cache = Some(Rc::new(ContextChain::new(
self.modifiers[1..].to_vec(),
self.executable.clone(),
)));
}
self.next_stage_cache.clone()
}
}
pub enum Stage {
Modify,
Execute,
}

View file

@ -1,5 +1,6 @@
mod command_context;
mod command_context_builder;
mod context_chain;
mod parsed_argument;
mod parsed_command_node;
mod string_range;
@ -7,6 +8,7 @@ pub mod suggestion_context;
pub use command_context::CommandContext;
pub use command_context_builder::CommandContextBuilder;
pub use context_chain::ContextChain;
pub use parsed_argument::ParsedArgument;
pub use parsed_command_node::ParsedCommandNode;
pub use string_range::StringRange;

View file

@ -1,10 +1,10 @@
use std::fmt;
use super::command_syntax_exception::CommandSyntaxException;
use super::command_syntax_error::CommandSyntaxError;
use crate::string_reader::StringReader;
#[derive(Clone, PartialEq)]
pub enum BuiltInExceptions {
pub enum BuiltInError {
DoubleTooSmall { found: f64, min: f64 },
DoubleTooBig { found: f64, max: f64 },
@ -40,114 +40,114 @@ pub enum BuiltInExceptions {
DispatcherParseException { message: String },
}
impl fmt::Debug for BuiltInExceptions {
impl fmt::Debug for BuiltInError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BuiltInExceptions::DoubleTooSmall { found, min } => {
BuiltInError::DoubleTooSmall { found, min } => {
write!(f, "Double must not be less than {min}, found {found}")
}
BuiltInExceptions::DoubleTooBig { found, max } => {
BuiltInError::DoubleTooBig { found, max } => {
write!(f, "Double must not be more than {max}, found {found}")
}
BuiltInExceptions::FloatTooSmall { found, min } => {
BuiltInError::FloatTooSmall { found, min } => {
write!(f, "Float must not be less than {min}, found {found}")
}
BuiltInExceptions::FloatTooBig { found, max } => {
BuiltInError::FloatTooBig { found, max } => {
write!(f, "Float must not be more than {max}, found {found}")
}
BuiltInExceptions::IntegerTooSmall { found, min } => {
BuiltInError::IntegerTooSmall { found, min } => {
write!(f, "Integer must not be less than {min}, found {found}")
}
BuiltInExceptions::IntegerTooBig { found, max } => {
BuiltInError::IntegerTooBig { found, max } => {
write!(f, "Integer must not be more than {max}, found {found}")
}
BuiltInExceptions::LongTooSmall { found, min } => {
BuiltInError::LongTooSmall { found, min } => {
write!(f, "Long must not be less than {min}, found {found}")
}
BuiltInExceptions::LongTooBig { found, max } => {
BuiltInError::LongTooBig { found, max } => {
write!(f, "Long must not be more than {max}, found {found}")
}
BuiltInExceptions::LiteralIncorrect { expected } => {
BuiltInError::LiteralIncorrect { expected } => {
write!(f, "Expected literal {expected}")
}
BuiltInExceptions::ReaderExpectedStartOfQuote => {
BuiltInError::ReaderExpectedStartOfQuote => {
write!(f, "Expected quote to start a string")
}
BuiltInExceptions::ReaderExpectedEndOfQuote => {
BuiltInError::ReaderExpectedEndOfQuote => {
write!(f, "Unclosed quoted string")
}
BuiltInExceptions::ReaderInvalidEscape { character } => {
BuiltInError::ReaderInvalidEscape { character } => {
write!(f, "Invalid escape sequence '{character}' in quoted string")
}
BuiltInExceptions::ReaderInvalidBool { value } => {
BuiltInError::ReaderInvalidBool { value } => {
write!(
f,
"Invalid bool, expected true or false but found '{value}'"
)
}
BuiltInExceptions::ReaderInvalidInt { value } => {
BuiltInError::ReaderInvalidInt { value } => {
write!(f, "Invalid Integer '{value}'")
}
BuiltInExceptions::ReaderExpectedInt => {
BuiltInError::ReaderExpectedInt => {
write!(f, "Expected Integer")
}
BuiltInExceptions::ReaderInvalidLong { value } => {
BuiltInError::ReaderInvalidLong { value } => {
write!(f, "Invalid long '{value}'")
}
BuiltInExceptions::ReaderExpectedLong => {
BuiltInError::ReaderExpectedLong => {
write!(f, "Expected long")
}
BuiltInExceptions::ReaderInvalidDouble { value } => {
BuiltInError::ReaderInvalidDouble { value } => {
write!(f, "Invalid double '{value}'")
}
BuiltInExceptions::ReaderExpectedDouble => {
BuiltInError::ReaderExpectedDouble => {
write!(f, "Expected double")
}
BuiltInExceptions::ReaderInvalidFloat { value } => {
BuiltInError::ReaderInvalidFloat { value } => {
write!(f, "Invalid Float '{value}'")
}
BuiltInExceptions::ReaderExpectedFloat => {
BuiltInError::ReaderExpectedFloat => {
write!(f, "Expected Float")
}
BuiltInExceptions::ReaderExpectedBool => {
BuiltInError::ReaderExpectedBool => {
write!(f, "Expected bool")
}
BuiltInExceptions::ReaderExpectedSymbol { symbol } => {
BuiltInError::ReaderExpectedSymbol { symbol } => {
write!(f, "Expected '{symbol}'")
}
BuiltInExceptions::DispatcherUnknownCommand => {
BuiltInError::DispatcherUnknownCommand => {
write!(f, "Unknown command")
}
BuiltInExceptions::DispatcherUnknownArgument => {
BuiltInError::DispatcherUnknownArgument => {
write!(f, "Incorrect argument for command")
}
BuiltInExceptions::DispatcherExpectedArgumentSeparator => {
BuiltInError::DispatcherExpectedArgumentSeparator => {
write!(
f,
"Expected whitespace to end one argument, but found trailing data"
)
}
BuiltInExceptions::DispatcherParseException { message } => {
BuiltInError::DispatcherParseException { message } => {
write!(f, "Could not parse command: {message}")
}
}
}
}
impl BuiltInExceptions {
pub fn create(self) -> CommandSyntaxException {
impl BuiltInError {
pub fn create(self) -> CommandSyntaxError {
let message = format!("{self:?}");
CommandSyntaxException::create(self, message)
CommandSyntaxError::create(self, message)
}
pub fn create_with_context(self, reader: &StringReader) -> CommandSyntaxException {
pub fn create_with_context(self, reader: &StringReader) -> CommandSyntaxError {
let message = format!("{self:?}");
CommandSyntaxException::new(self, message, reader.string(), reader.cursor())
CommandSyntaxError::new(self, message, reader.string(), reader.cursor())
}
}

View file

@ -0,0 +1,93 @@
use std::{
cmp,
fmt::{self, Debug, Write},
};
use super::builtin_errors::BuiltInError;
#[derive(Clone, PartialEq)]
pub struct CommandSyntaxError {
kind: BuiltInError,
message: String,
input: Option<String>,
cursor: Option<usize>,
}
const CONTEXT_AMOUNT: usize = 10;
impl CommandSyntaxError {
pub fn new(kind: BuiltInError, message: String, input: &str, cursor: usize) -> Self {
Self {
kind,
message,
input: Some(input.to_string()),
cursor: Some(cursor),
}
}
pub fn create(kind: BuiltInError, message: String) -> Self {
Self {
kind,
message,
input: None,
cursor: None,
}
}
pub fn message(&self) -> String {
let mut message = self.message.clone();
let context = self.context();
if let Some(context) = context {
write!(
message,
" at position {}: {context}",
self.cursor.unwrap_or(usize::MAX)
)
.unwrap();
}
message
}
pub fn raw_message(&self) -> &String {
&self.message
}
pub fn context(&self) -> Option<String> {
if let Some(input) = &self.input
&& let Some(cursor) = self.cursor
{
let mut builder = String::new();
let cursor = cmp::min(input.len(), cursor);
if cursor > CONTEXT_AMOUNT {
builder.push_str("...");
}
builder.push_str(
&input[(cmp::max(0, cursor as isize - CONTEXT_AMOUNT as isize) as usize)..cursor],
);
builder.push_str("<--[HERE]");
return Some(builder);
}
None
}
pub fn kind(&self) -> &BuiltInError {
&self.kind
}
pub fn input(&self) -> &Option<String> {
&self.input
}
pub fn cursor(&self) -> Option<usize> {
self.cursor
}
}
impl Debug for CommandSyntaxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message())
}
}

View file

@ -0,0 +1,5 @@
mod builtin_errors;
mod command_syntax_error;
pub use builtin_errors::BuiltInError;
pub use command_syntax_error::CommandSyntaxError;

View file

@ -1,95 +0,0 @@
use std::{
cmp,
fmt::{self, Write},
};
use super::builtin_exceptions::BuiltInExceptions;
#[derive(Clone, PartialEq)]
pub struct CommandSyntaxException {
pub type_: BuiltInExceptions,
message: String,
input: Option<String>,
cursor: Option<usize>,
}
const CONTEXT_AMOUNT: usize = 10;
impl CommandSyntaxException {
pub fn new(type_: BuiltInExceptions, message: String, input: &str, cursor: usize) -> Self {
Self {
type_,
message,
input: Some(input.to_string()),
cursor: Some(cursor),
}
}
pub fn create(type_: BuiltInExceptions, message: String) -> Self {
Self {
type_,
message,
input: None,
cursor: None,
}
}
pub fn message(&self) -> String {
let mut message = self.message.clone();
let context = self.context();
if let Some(context) = context {
write!(
message,
" at position {}: {}",
self.cursor.unwrap_or(usize::MAX),
context
)
.unwrap();
}
message
}
pub fn raw_message(&self) -> &String {
&self.message
}
pub fn context(&self) -> Option<String> {
if let Some(input) = &self.input {
if let Some(cursor) = self.cursor {
let mut builder = String::new();
let cursor = cmp::min(input.len(), cursor);
if cursor > CONTEXT_AMOUNT {
builder.push_str("...");
}
builder.push_str(
&input
[(cmp::max(0, cursor as isize - CONTEXT_AMOUNT as isize) as usize)..cursor],
);
builder.push_str("<--[HERE]");
return Some(builder);
}
}
None
}
pub fn get_type(&self) -> &BuiltInExceptions {
&self.type_
}
pub fn input(&self) -> &Option<String> {
&self.input
}
pub fn cursor(&self) -> Option<usize> {
self.cursor
}
}
impl fmt::Debug for CommandSyntaxException {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message())
}
}

View file

@ -1,5 +0,0 @@
mod builtin_exceptions;
mod command_syntax_exception;
pub use builtin_exceptions::BuiltInExceptions;
pub use command_syntax_exception::CommandSyntaxException;

View file

@ -4,9 +4,10 @@ pub mod arguments;
pub mod builder;
pub mod command_dispatcher;
pub mod context;
pub mod exceptions;
pub mod errors;
pub mod modifier;
pub mod parse_results;
pub mod result_consumer;
pub mod string_reader;
pub mod suggestion;
pub mod tree;

View file

@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::{context::CommandContext, exceptions::CommandSyntaxException};
use crate::{context::CommandContext, errors::CommandSyntaxError};
pub type RedirectModifier<S> =
dyn Fn(&CommandContext<S>) -> Result<Vec<Arc<S>>, CommandSyntaxException> + Send + Sync;
dyn Fn(&CommandContext<S>) -> Result<Vec<Arc<S>>, CommandSyntaxError> + Send + Sync;

View file

@ -1,18 +1,22 @@
use std::{collections::HashMap, fmt::Debug, rc::Rc};
use std::{
collections::HashMap,
fmt::{self, Debug},
rc::Rc,
};
use crate::{
context::CommandContextBuilder, exceptions::CommandSyntaxException,
string_reader::StringReader, tree::CommandNode,
context::CommandContextBuilder, errors::CommandSyntaxError, string_reader::StringReader,
tree::CommandNode,
};
pub struct ParseResults<'a, S> {
pub context: CommandContextBuilder<'a, S>,
pub reader: StringReader,
pub exceptions: HashMap<Rc<CommandNode<S>>, CommandSyntaxException>,
pub exceptions: HashMap<Rc<CommandNode<S>>, CommandSyntaxError>,
}
impl<S> Debug for ParseResults<'_, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ParseResults")
.field("context", &self.context)
// .field("reader", &self.reader)

View file

@ -0,0 +1,12 @@
use std::rc::Rc;
use crate::context::CommandContext;
pub trait ResultConsumer<S> {
fn on_command_complete(&self, context: Rc<CommandContext<S>>, success: bool, result: i32);
}
pub struct DefaultResultConsumer;
impl<S> ResultConsumer<S> for DefaultResultConsumer {
fn on_command_complete(&self, _context: Rc<CommandContext<S>>, _success: bool, _result: i32) {}
}

View file

@ -1,6 +1,6 @@
use std::str::FromStr;
use crate::exceptions::{BuiltInExceptions, CommandSyntaxException};
use crate::errors::{BuiltInError, CommandSyntaxError};
#[derive(Clone)]
pub struct StringReader {
@ -91,19 +91,19 @@ impl StringReader {
}
}
pub fn read_int(&mut self) -> Result<i32, CommandSyntaxException> {
pub fn read_int(&mut self) -> Result<i32, CommandSyntaxError> {
let start = self.cursor;
while self.can_read() && StringReader::is_allowed_number(self.peek()) {
self.skip();
}
let number = &self.string[start..self.cursor];
if number.is_empty() {
return Err(BuiltInExceptions::ReaderExpectedInt.create_with_context(self));
return Err(BuiltInError::ReaderExpectedInt.create_with_context(self));
}
let result = i32::from_str(number);
if result.is_err() {
self.cursor = start;
return Err(BuiltInExceptions::ReaderInvalidInt {
return Err(BuiltInError::ReaderInvalidInt {
value: number.to_string(),
}
.create_with_context(self));
@ -112,19 +112,19 @@ impl StringReader {
Ok(result.unwrap())
}
pub fn read_long(&mut self) -> Result<i64, CommandSyntaxException> {
pub fn read_long(&mut self) -> Result<i64, CommandSyntaxError> {
let start = self.cursor;
while self.can_read() && StringReader::is_allowed_number(self.peek()) {
self.skip();
}
let number = &self.string[start..self.cursor];
if number.is_empty() {
return Err(BuiltInExceptions::ReaderExpectedLong.create_with_context(self));
return Err(BuiltInError::ReaderExpectedLong.create_with_context(self));
}
let result = i64::from_str(number);
if result.is_err() {
self.cursor = start;
return Err(BuiltInExceptions::ReaderInvalidLong {
return Err(BuiltInError::ReaderInvalidLong {
value: number.to_string(),
}
.create_with_context(self));
@ -133,19 +133,19 @@ impl StringReader {
Ok(result.unwrap())
}
pub fn read_double(&mut self) -> Result<f64, CommandSyntaxException> {
pub fn read_double(&mut self) -> Result<f64, CommandSyntaxError> {
let start = self.cursor;
while self.can_read() && StringReader::is_allowed_number(self.peek()) {
self.skip();
}
let number = &self.string[start..self.cursor];
if number.is_empty() {
return Err(BuiltInExceptions::ReaderExpectedDouble.create_with_context(self));
return Err(BuiltInError::ReaderExpectedDouble.create_with_context(self));
}
let result = f64::from_str(number);
if result.is_err() {
self.cursor = start;
return Err(BuiltInExceptions::ReaderInvalidDouble {
return Err(BuiltInError::ReaderInvalidDouble {
value: number.to_string(),
}
.create_with_context(self));
@ -154,19 +154,19 @@ impl StringReader {
Ok(result.unwrap())
}
pub fn read_float(&mut self) -> Result<f32, CommandSyntaxException> {
pub fn read_float(&mut self) -> Result<f32, CommandSyntaxError> {
let start = self.cursor;
while self.can_read() && StringReader::is_allowed_number(self.peek()) {
self.skip();
}
let number = &self.string[start..self.cursor];
if number.is_empty() {
return Err(BuiltInExceptions::ReaderExpectedFloat.create_with_context(self));
return Err(BuiltInError::ReaderExpectedFloat.create_with_context(self));
}
let result = f32::from_str(number);
if result.is_err() {
self.cursor = start;
return Err(BuiltInExceptions::ReaderInvalidFloat {
return Err(BuiltInError::ReaderInvalidFloat {
value: number.to_string(),
}
.create_with_context(self));
@ -193,22 +193,19 @@ impl StringReader {
&self.string[start..self.cursor]
}
pub fn read_quoted_string(&mut self) -> Result<String, CommandSyntaxException> {
pub fn read_quoted_string(&mut self) -> Result<String, CommandSyntaxError> {
if !self.can_read() {
return Ok(String::new());
}
let next = self.peek();
if !StringReader::is_quoted_string_start(next) {
return Err(BuiltInExceptions::ReaderExpectedStartOfQuote.create_with_context(self));
return Err(BuiltInError::ReaderExpectedStartOfQuote.create_with_context(self));
}
self.skip();
self.read_string_until(next)
}
pub fn read_string_until(
&mut self,
terminator: char,
) -> Result<String, CommandSyntaxException> {
pub fn read_string_until(&mut self, terminator: char) -> Result<String, CommandSyntaxError> {
let mut result = String::new();
let mut escaped = false;
while self.can_read() {
@ -219,7 +216,7 @@ impl StringReader {
escaped = false;
} else {
self.cursor -= 1;
return Err(BuiltInExceptions::ReaderInvalidEscape { character: c }
return Err(BuiltInError::ReaderInvalidEscape { character: c }
.create_with_context(self));
}
} else if c == SYNTAX_ESCAPE {
@ -231,10 +228,10 @@ impl StringReader {
}
}
Err(BuiltInExceptions::ReaderExpectedEndOfQuote.create_with_context(self))
Err(BuiltInError::ReaderExpectedEndOfQuote.create_with_context(self))
}
pub fn read_string(&mut self) -> Result<String, CommandSyntaxException> {
pub fn read_string(&mut self) -> Result<String, CommandSyntaxError> {
if !self.can_read() {
return Ok(String::new());
}
@ -246,11 +243,11 @@ impl StringReader {
Ok(self.read_unquoted_string().to_string())
}
pub fn read_boolean(&mut self) -> Result<bool, CommandSyntaxException> {
pub fn read_boolean(&mut self) -> Result<bool, CommandSyntaxError> {
let start = self.cursor;
let value = self.read_string()?;
if value.is_empty() {
return Err(BuiltInExceptions::ReaderExpectedBool.create_with_context(self));
return Err(BuiltInError::ReaderExpectedBool.create_with_context(self));
}
if value == "true" {
@ -259,15 +256,13 @@ impl StringReader {
Ok(false)
} else {
self.cursor = start;
Err(BuiltInExceptions::ReaderInvalidBool { value }.create_with_context(self))
Err(BuiltInError::ReaderInvalidBool { value }.create_with_context(self))
}
}
pub fn expect(&mut self, c: char) -> Result<(), CommandSyntaxException> {
pub fn expect(&mut self, c: char) -> Result<(), CommandSyntaxError> {
if !self.can_read() || self.peek() != c {
return Err(
BuiltInExceptions::ReaderExpectedSymbol { symbol: c }.create_with_context(self)
);
return Err(BuiltInError::ReaderExpectedSymbol { symbol: c }.create_with_context(self));
}
self.skip();
Ok(())

View file

@ -1,9 +1,11 @@
mod suggestion_provider;
mod suggestions;
mod suggestions_builder;
#[cfg(feature = "azalea-buf")]
use std::io::Write;
use std::io::{self, Write};
use std::{
cmp::Ordering,
fmt::{self, Display},
hash::Hash,
};
@ -12,6 +14,7 @@ use std::{
use azalea_buf::AzaleaWrite;
#[cfg(feature = "azalea-buf")]
use azalea_chat::FormattedText;
pub use suggestion_provider::SuggestionProvider;
pub use suggestions::Suggestions;
pub use suggestions_builder::SuggestionsBuilder;
@ -93,7 +96,7 @@ impl Suggestion {
}
impl SuggestionValue {
pub fn cmp_ignore_case(&self, other: &Self) -> std::cmp::Ordering {
pub fn cmp_ignore_case(&self, other: &Self) -> Ordering {
match (self, other) {
(SuggestionValue::Text(a), SuggestionValue::Text(b)) => {
a.to_lowercase().cmp(&b.to_lowercase())
@ -118,7 +121,7 @@ impl Display for SuggestionValue {
}
impl Ord for SuggestionValue {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(SuggestionValue::Text(a), SuggestionValue::Text(b)) => a.cmp(b),
(SuggestionValue::Integer(a), SuggestionValue::Integer(b)) => a.cmp(b),
@ -131,14 +134,14 @@ impl Ord for SuggestionValue {
}
}
impl PartialOrd for SuggestionValue {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(feature = "azalea-buf")]
impl AzaleaWrite for Suggestion {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
self.value.to_string().azalea_write(buf)?;
self.tooltip
.clone()

View file

@ -0,0 +1,10 @@
use super::{Suggestions, SuggestionsBuilder};
use crate::context::CommandContext;
pub trait SuggestionProvider<S> {
fn get_suggestions(
&self,
context: CommandContext<S>,
builder: SuggestionsBuilder,
) -> Suggestions;
}

View file

@ -1,5 +1,5 @@
#[cfg(feature = "azalea-buf")]
use std::io::{Cursor, Write};
use std::io::{self, Cursor, Write};
use std::{collections::HashSet, hash::Hash};
#[cfg(feature = "azalea-buf")]
@ -107,7 +107,7 @@ impl AzaleaRead for Suggestions {
#[cfg(feature = "azalea-buf")]
impl AzaleaWrite for Suggestions {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
(self.range.start() as u32).azalea_write_var(buf)?;
(self.range.length() as u32).azalea_write_var(buf)?;
self.suggestions.azalea_write(buf)?;

View file

@ -1,7 +1,7 @@
use std::{
collections::{BTreeMap, HashMap},
fmt::Debug,
hash::Hash,
fmt::{self, Debug},
hash::{Hash, Hasher},
ptr,
sync::Arc,
};
@ -14,18 +14,19 @@ use crate::{
required_argument_builder::Argument,
},
context::{CommandContext, CommandContextBuilder, ParsedArgument, StringRange},
exceptions::{BuiltInExceptions, CommandSyntaxException},
errors::{BuiltInError, CommandSyntaxError},
modifier::RedirectModifier,
string_reader::StringReader,
suggestion::{Suggestions, SuggestionsBuilder},
};
pub type Command<S> = Option<Arc<dyn Fn(&CommandContext<S>) -> i32 + Send + Sync>>;
pub type Command<S> =
Option<Arc<dyn Fn(&CommandContext<S>) -> Result<i32, CommandSyntaxError> + Send + Sync>>;
/// An ArgumentBuilder that has been built.
#[non_exhaustive]
pub struct CommandNode<S> {
pub value: ArgumentBuilderType,
pub value: ArgumentBuilderType<S>,
// this is a BTreeMap because children need to be ordered when getting command suggestions
pub children: BTreeMap<String, Arc<RwLock<CommandNode<S>>>>,
@ -66,7 +67,7 @@ impl<S> CommandNode<S> {
}
/// Gets the argument, or panics. You should use match if you're not certain
/// about the type.
pub fn argument(&self) -> &Argument {
pub fn argument(&self) -> &Argument<S> {
match self.value {
ArgumentBuilderType::Argument(ref argument) => argument,
_ => panic!("CommandNode::argument() called on non-argument node"),
@ -149,7 +150,7 @@ impl<S> CommandNode<S> {
&self,
reader: &mut StringReader,
context_builder: &mut CommandContextBuilder<S>,
) -> Result<(), CommandSyntaxException> {
) -> Result<(), CommandSyntaxError> {
match self.value {
ArgumentBuilderType::Argument(ref argument) => {
let start = reader.cursor();
@ -176,7 +177,7 @@ impl<S> CommandNode<S> {
return Ok(());
}
Err(BuiltInExceptions::LiteralIncorrect {
Err(BuiltInError::LiteralIncorrect {
expected: literal.value.clone(),
}
.create_with_context(reader))
@ -214,9 +215,7 @@ impl<S> CommandNode<S> {
pub fn list_suggestions(
&self,
// context is here because that's how it is in mojang's brigadier, but we haven't
// implemented custom suggestions yet so this is unused rn
_context: CommandContext<S>,
context: CommandContext<S>,
builder: SuggestionsBuilder,
) -> Suggestions {
match &self.value {
@ -231,15 +230,15 @@ impl<S> CommandNode<S> {
Suggestions::default()
}
}
ArgumentBuilderType::Argument(argument) => argument.list_suggestions(builder),
ArgumentBuilderType::Argument(argument) => argument.list_suggestions(context, builder),
}
}
}
impl<S> Debug for CommandNode<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CommandNode")
.field("value", &self.value)
// .field("value", &self.value)
.field("children", &self.children)
.field("command", &self.command.is_some())
// .field("requirement", &self.requirement)
@ -269,7 +268,7 @@ impl<S> Default for CommandNode<S> {
}
impl<S> Hash for CommandNode<S> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
fn hash<H: Hasher>(&self, state: &mut H) {
// hash the children
for (k, v) in &self.children {
k.hash(state);

View file

@ -1,18 +1,8 @@
use std::sync::Arc;
use std::{mem, ops::Deref, sync::Arc};
use azalea_brigadier::{
arguments::integer_argument_type::integer,
builder::{literal_argument_builder::literal, required_argument_builder::argument},
command_dispatcher::CommandDispatcher,
context::CommandContext,
};
use azalea_brigadier::prelude::*;
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]
@ -150,8 +140,7 @@ impl DispatchStorage {
///
/// Spawns a number of entities with the [`SpawnedEntity`] component.
fn command_spawn_entity_num(context: &CommandContext<WorldAccessor>) -> i32 {
let num = context.argument("entities").unwrap();
let num = *num.downcast_ref::<i32>().unwrap();
let num = get_integer(context, "entities").unwrap();
for _ in 0..num {
context.source.lock().spawn(SpawnedEntity);
@ -183,7 +172,7 @@ impl WorldAccessor {
/// Swap the internal [`World`] with the given one.
fn swap(&mut self, world: &mut World) {
std::mem::swap(&mut *self.lock(), world);
mem::swap(&mut *self.lock(), world);
}
}
@ -192,7 +181,7 @@ impl WorldAccessor {
struct SpawnedEntity;
/// Implemented for convenience.
impl std::ops::Deref for WorldAccessor {
impl Deref for WorldAccessor {
type Target = Arc<Mutex<World>>;
fn deref(&self) -> &Self::Target {
&self.world

View file

@ -5,7 +5,7 @@ use azalea_brigadier::{
builder::{literal_argument_builder::literal, required_argument_builder::argument},
command_dispatcher::CommandDispatcher,
context::CommandContext,
exceptions::{BuiltInExceptions, CommandSyntaxException},
errors::{BuiltInError, CommandSyntaxError},
string_reader::StringReader,
};
@ -50,10 +50,7 @@ fn execute_unknown_command() {
let execute_result = subject.execute("foo", &CommandSource {});
let err = execute_result.err().unwrap();
match err.type_ {
BuiltInExceptions::DispatcherUnknownCommand => {}
_ => panic!("Unexpected error"),
}
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownCommand);
assert_eq!(err.cursor().unwrap(), 0);
}
@ -65,10 +62,7 @@ fn execute_impermissible_command() {
let execute_result = subject.execute("foo", &CommandSource {});
let err = execute_result.err().unwrap();
match err.type_ {
BuiltInExceptions::DispatcherUnknownCommand => {}
_ => panic!("Unexpected error"),
}
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownCommand);
assert_eq!(err.cursor().unwrap(), 0);
}
@ -80,10 +74,7 @@ fn execute_empty_command() {
let execute_result = subject.execute("", &CommandSource {});
let err = execute_result.err().unwrap();
match err.type_ {
BuiltInExceptions::DispatcherUnknownCommand => {}
_ => panic!("Unexpected error"),
}
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownCommand);
assert_eq!(err.cursor().unwrap(), 0);
}
@ -95,10 +86,7 @@ fn execute_unknown_subcommand() {
let execute_result = subject.execute("foo bar", &CommandSource {});
let err = execute_result.err().unwrap();
match err.type_ {
BuiltInExceptions::DispatcherUnknownArgument => {}
_ => panic!("Unexpected error"),
}
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownArgument);
assert_eq!(err.cursor().unwrap(), 4);
}
@ -110,10 +98,7 @@ fn execute_incorrect_literal() {
let execute_result = subject.execute("foo baz", &CommandSource {});
let err = execute_result.err().unwrap();
match err.type_ {
BuiltInExceptions::DispatcherUnknownArgument => {}
_ => panic!("Unexpected error"),
}
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownArgument);
assert_eq!(err.cursor().unwrap(), 4);
}
@ -130,10 +115,7 @@ fn execute_ambiguous_incorrect_argument() {
let execute_result = subject.execute("foo unknown", &CommandSource {});
let err = execute_result.err().unwrap();
match err.type_ {
BuiltInExceptions::DispatcherUnknownArgument => {}
_ => panic!("Unexpected error"),
}
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownArgument);
assert_eq!(err.cursor().unwrap(), 4);
}
@ -173,7 +155,7 @@ fn parse_incomplete_argument() {
}
#[test]
fn execute_ambiguious_parent_subcommand() {
fn execute_ambiguous_parent_subcommand() {
let mut subject = CommandDispatcher::new();
subject.register(
@ -186,7 +168,7 @@ fn execute_ambiguious_parent_subcommand() {
}
#[test]
fn execute_ambiguious_parent_subcommand_via_redirect() {
fn execute_ambiguous_parent_subcommand_via_redirect() {
let mut subject = CommandDispatcher::new();
let real = subject.register(
@ -245,7 +227,7 @@ fn execute_redirected_multiple_times() {
);
assert_eq!(*child2.unwrap().nodes[0].node.read(), *concrete_node.read());
assert_eq!(CommandDispatcher::execute_parsed(parse).unwrap(), 42);
assert_eq!(subject.execute_parsed(parse).unwrap(), 42);
}
#[test]
@ -255,7 +237,7 @@ fn execute_redirected() {
let source1 = Arc::new(CommandSource {});
let source2 = Arc::new(CommandSource {});
let modifier = move |_: &CommandContext<CommandSource>| -> Result<Vec<Arc<CommandSource>>, CommandSyntaxException> {
let modifier = move |_: &CommandContext<CommandSource>| -> Result<Vec<Arc<CommandSource>>, CommandSyntaxError> {
Ok(vec![source1.clone(), source2.clone()])
};
@ -281,7 +263,7 @@ fn execute_redirected() {
assert_eq!(*parent.nodes[0].node.read(), *concrete_node.read());
assert_eq!(*parent.source, CommandSource {});
assert_eq!(CommandDispatcher::execute_parsed(parse).unwrap(), 2);
assert_eq!(subject.execute_parsed(parse).unwrap(), 2);
}
#[test]
@ -297,10 +279,7 @@ fn execute_orphaned_subcommand() {
let result = subject.execute("foo 5", &CommandSource {});
assert!(result.is_err());
let result = result.unwrap_err();
assert_eq!(
*result.get_type(),
BuiltInExceptions::DispatcherUnknownCommand
);
assert_eq!(*result.kind(), BuiltInError::DispatcherUnknownCommand);
assert_eq!(result.cursor(), Some(5));
}
@ -327,10 +306,7 @@ fn parse_no_space_separator() {
let result = subject.execute("foo$", &CommandSource {});
assert!(result.is_err());
let result = result.unwrap_err();
assert_eq!(
*result.get_type(),
BuiltInExceptions::DispatcherUnknownCommand
);
assert_eq!(*result.kind(), BuiltInError::DispatcherUnknownCommand);
assert_eq!(result.cursor(), Some(0));
}
@ -348,7 +324,7 @@ fn execute_invalid_subcommand() {
assert!(result.is_err());
let result = result.unwrap_err();
// this fails for some reason, i blame mojang
// assert_eq!(*result.get_type(), BuiltInExceptions::ReaderExpectedInt);
// assert_eq!(*result.get_type(), BuiltInError::ReaderExpectedInt);
assert_eq!(result.cursor(), Some(4));
}

View file

@ -1,4 +1,4 @@
use azalea_brigadier::{exceptions::BuiltInExceptions, string_reader::StringReader};
use azalea_brigadier::{errors::BuiltInError, string_reader::StringReader};
#[test]
fn can_read() {
@ -222,7 +222,7 @@ fn read_quoted_string_no_open() {
let result = reader.read_quoted_string();
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedStartOfQuote);
assert_eq!(e.kind(), &BuiltInError::ReaderExpectedStartOfQuote);
assert_eq!(e.cursor(), Some(0));
}
}
@ -233,7 +233,7 @@ fn read_quoted_string_no_close() {
let result = reader.read_quoted_string();
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedEndOfQuote);
assert_eq!(e.kind(), &BuiltInError::ReaderExpectedEndOfQuote);
assert_eq!(e.cursor(), Some(12));
}
}
@ -245,8 +245,8 @@ fn read_quoted_string_invalid_escape() {
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(
e.get_type(),
&BuiltInExceptions::ReaderInvalidEscape { character: 'n' }
e.kind(),
&BuiltInError::ReaderInvalidEscape { character: 'n' }
);
assert_eq!(e.cursor(), Some(7));
}
@ -259,8 +259,8 @@ fn read_quoted_string_invalid_quote_escape() {
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(
e.get_type(),
&BuiltInExceptions::ReaderInvalidEscape { character: '"' }
e.kind(),
&BuiltInError::ReaderInvalidEscape { character: '"' }
);
assert_eq!(e.cursor(), Some(7));
}
@ -313,8 +313,8 @@ fn read_int_invalid() {
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(
e.get_type(),
&BuiltInExceptions::ReaderInvalidInt {
e.kind(),
&BuiltInError::ReaderInvalidInt {
value: "12.34".to_string()
}
);
@ -328,7 +328,7 @@ fn read_int_none() {
let result = reader.read_int();
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedInt);
assert_eq!(e.kind(), &BuiltInError::ReaderExpectedInt);
assert_eq!(e.cursor(), Some(0));
}
}
@ -372,8 +372,8 @@ fn read_long_invalid() {
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(
e.get_type(),
&BuiltInExceptions::ReaderInvalidLong {
e.kind(),
&BuiltInError::ReaderInvalidLong {
value: "12.34".to_string()
}
);
@ -387,7 +387,7 @@ fn read_long_none() {
let result = reader.read_long();
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedLong);
assert_eq!(e.kind(), &BuiltInError::ReaderExpectedLong);
assert_eq!(e.cursor(), Some(0));
}
}
@ -439,8 +439,8 @@ fn read_double_invalid() {
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(
e.get_type(),
&BuiltInExceptions::ReaderInvalidDouble {
e.kind(),
&BuiltInError::ReaderInvalidDouble {
value: "12.34.56".to_string()
}
);
@ -454,7 +454,7 @@ fn read_double_none() {
let result = reader.read_double();
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedDouble);
assert_eq!(e.kind(), &BuiltInError::ReaderExpectedDouble);
assert_eq!(e.cursor(), Some(0));
}
}
@ -506,8 +506,8 @@ fn read_float_invalid() {
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(
e.get_type(),
&BuiltInExceptions::ReaderInvalidFloat {
e.kind(),
&BuiltInError::ReaderInvalidFloat {
value: "12.34.56".to_string()
}
);
@ -521,7 +521,7 @@ fn read_float_none() {
let result = reader.read_float();
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedFloat);
assert_eq!(e.kind(), &BuiltInError::ReaderExpectedFloat);
assert_eq!(e.cursor(), Some(0));
}
}
@ -556,8 +556,8 @@ fn expect_incorrect() {
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(
e.get_type(),
&BuiltInExceptions::ReaderExpectedSymbol { symbol: 'a' }
e.kind(),
&BuiltInError::ReaderExpectedSymbol { symbol: 'a' }
);
assert_eq!(e.cursor(), Some(0));
}
@ -570,8 +570,8 @@ fn expect_none() {
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(
e.get_type(),
&BuiltInExceptions::ReaderExpectedSymbol { symbol: 'a' }
e.kind(),
&BuiltInError::ReaderExpectedSymbol { symbol: 'a' }
);
assert_eq!(e.cursor(), Some(0));
}
@ -591,8 +591,8 @@ fn read_boolean_incorrect() {
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(
e.get_type(),
&BuiltInExceptions::ReaderInvalidBool {
e.kind(),
&BuiltInError::ReaderInvalidBool {
value: "tuesday".to_string()
}
);
@ -606,7 +606,7 @@ fn read_boolean_none() {
let result = reader.read_boolean();
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedBool);
assert_eq!(e.kind(), &BuiltInError::ReaderExpectedBool);
assert_eq!(e.cursor(), Some(0));
}
}

View file

@ -17,7 +17,7 @@ fn merge_single() {
StringRange::at(5),
vec![Suggestion::new(StringRange::at(5), "ar")],
);
let merged = Suggestions::merge("foo b", &[suggestions.clone()]);
let merged = Suggestions::merge("foo b", std::slice::from_ref(&suggestions));
assert_eq!(merged, suggestions);
}

View file

@ -7,7 +7,7 @@ license.workspace = true
repository.workspace = true
[dependencies]
azalea-buf-macros = { path = "./azalea-buf-macros", version = "0.12.0" }
azalea-buf-macros.workspace = true
byteorder.workspace = true
serde_json = { workspace = true, optional = true }
simdnbt.workspace = true

View file

@ -2,7 +2,7 @@ use std::{
backtrace::Backtrace,
collections::HashMap,
hash::Hash,
io::{Cursor, Read},
io::{self, Cursor, Read},
sync::Arc,
};
@ -30,7 +30,7 @@ pub enum BufReadError {
Io {
#[from]
#[backtrace]
source: std::io::Error,
source: io::Error,
},
#[error("Invalid UTF-8: {bytes:?} (lossy: {lossy:?})")]
InvalidUtf8 {

View file

@ -1,4 +1,4 @@
use std::io::{Cursor, Write};
use std::io::{self, Cursor, Write};
use uuid::Uuid;
@ -46,7 +46,7 @@ impl AzaleaRead for Uuid {
}
impl AzaleaWrite for Uuid {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
let [a, b, c, d] = self.to_int_array();
a.azalea_write(buf)?;
b.azalea_write(buf)?;

View file

@ -8,7 +8,7 @@ use byteorder::{BigEndian, WriteBytesExt};
use super::{MAX_STRING_LENGTH, UnsizedByteArray};
fn write_utf_with_len(buf: &mut impl Write, string: &str, len: usize) -> Result<(), io::Error> {
fn write_utf_with_len(buf: &mut impl Write, string: &str, len: usize) -> io::Result<()> {
if string.len() > len {
panic!(
"String too big (was {} bytes encoded, max {})",
@ -21,25 +21,25 @@ fn write_utf_with_len(buf: &mut impl Write, string: &str, len: usize) -> Result<
}
pub trait AzaleaWrite {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error>;
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()>;
}
pub trait AzaleaWriteVar {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error>;
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()>;
}
impl AzaleaWrite for i32 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
WriteBytesExt::write_i32::<BigEndian>(buf, *self)
}
}
impl AzaleaWriteVar for i32 {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> {
let mut buffer = [0];
let mut value = *self;
if value == 0 {
buf.write_all(&buffer).unwrap();
buf.write_all(&buffer)?;
}
while value != 0 {
buffer[0] = (value & 0b0111_1111) as u8;
@ -54,24 +54,24 @@ impl AzaleaWriteVar for i32 {
}
impl AzaleaWrite for UnsizedByteArray {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
buf.write_all(self)
}
}
impl<T: AzaleaWrite> AzaleaWrite for Vec<T> {
default fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
default fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
self[..].azalea_write(buf)
}
}
impl<T: AzaleaWrite> AzaleaWrite for Box<[T]> {
default fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
default fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
self[..].azalea_write(buf)
}
}
impl<T: AzaleaWrite> AzaleaWrite for [T] {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
(self.len() as u32).azalea_write_var(buf)?;
for item in self {
T::azalea_write(item, buf)?;
@ -81,7 +81,7 @@ impl<T: AzaleaWrite> AzaleaWrite for [T] {
}
impl<K: AzaleaWrite, V: AzaleaWrite> AzaleaWrite for HashMap<K, V> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
u32::azalea_write_var(&(self.len() as u32), buf)?;
for (key, value) in self {
key.azalea_write(buf)?;
@ -93,7 +93,7 @@ impl<K: AzaleaWrite, V: AzaleaWrite> AzaleaWrite for HashMap<K, V> {
}
impl<K: AzaleaWrite, V: AzaleaWriteVar> AzaleaWriteVar for HashMap<K, V> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> {
u32::azalea_write_var(&(self.len() as u32), buf)?;
for (key, value) in self {
key.azalea_write(buf)?;
@ -105,38 +105,38 @@ impl<K: AzaleaWrite, V: AzaleaWriteVar> AzaleaWriteVar for HashMap<K, V> {
}
impl AzaleaWrite for Vec<u8> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
(self.len() as u32).azalea_write_var(buf)?;
buf.write_all(self)
}
}
impl AzaleaWrite for String {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
write_utf_with_len(buf, self, MAX_STRING_LENGTH.into())
}
}
impl AzaleaWrite for &str {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
write_utf_with_len(buf, self, MAX_STRING_LENGTH.into())
}
}
impl AzaleaWrite for u32 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
i32::azalea_write(&(*self as i32), buf)
}
}
impl AzaleaWriteVar for u32 {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> {
i32::azalea_write_var(&(*self as i32), buf)
}
}
impl AzaleaWriteVar for i64 {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> {
let mut buffer = [0];
let mut value = *self;
if value == 0 {
@ -155,25 +155,25 @@ impl AzaleaWriteVar for i64 {
}
impl AzaleaWriteVar for u64 {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> {
i64::azalea_write_var(&(*self as i64), buf)
}
}
impl AzaleaWrite for u16 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
i16::azalea_write(&(*self as i16), buf)
}
}
impl AzaleaWriteVar for u16 {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> {
i32::azalea_write_var(&(*self as i32), buf)
}
}
impl<T: AzaleaWriteVar> AzaleaWriteVar for [T] {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> {
u32::azalea_write_var(&(self.len() as u32), buf)?;
for i in self {
i.azalea_write_var(buf)?;
@ -182,67 +182,67 @@ impl<T: AzaleaWriteVar> AzaleaWriteVar for [T] {
}
}
impl<T: AzaleaWriteVar> AzaleaWriteVar for Vec<T> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> {
self[..].azalea_write_var(buf)
}
}
impl<T: AzaleaWriteVar> AzaleaWriteVar for Box<[T]> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> {
self[..].azalea_write_var(buf)
}
}
impl AzaleaWrite for u8 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
WriteBytesExt::write_u8(buf, *self)
}
}
impl AzaleaWrite for i16 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
WriteBytesExt::write_i16::<BigEndian>(buf, *self)
}
}
impl AzaleaWrite for i64 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
WriteBytesExt::write_i64::<BigEndian>(buf, *self)
}
}
impl AzaleaWrite for u64 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
i64::azalea_write(&(*self as i64), buf)
}
}
impl AzaleaWrite for bool {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
let byte = u8::from(*self);
byte.azalea_write(buf)
}
}
impl AzaleaWrite for i8 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
(*self as u8).azalea_write(buf)
}
}
impl AzaleaWrite for f32 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
WriteBytesExt::write_f32::<BigEndian>(buf, *self)
}
}
impl AzaleaWrite for f64 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
WriteBytesExt::write_f64::<BigEndian>(buf, *self)
}
}
impl<T: AzaleaWrite> AzaleaWrite for Option<T> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
if let Some(s) = self {
true.azalea_write(buf)?;
s.azalea_write(buf)?;
@ -254,7 +254,7 @@ impl<T: AzaleaWrite> AzaleaWrite for Option<T> {
}
impl<T: AzaleaWriteVar> AzaleaWriteVar for Option<T> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> {
if let Some(s) = self {
true.azalea_write(buf)?;
s.azalea_write_var(buf)?;
@ -267,7 +267,7 @@ impl<T: AzaleaWriteVar> AzaleaWriteVar for Option<T> {
// [T; N]
impl<T: AzaleaWrite, const N: usize> AzaleaWrite for [T; N] {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
for i in self {
i.azalea_write(buf)?;
}
@ -276,7 +276,7 @@ impl<T: AzaleaWrite, const N: usize> AzaleaWrite for [T; N] {
}
impl AzaleaWrite for simdnbt::owned::NbtTag {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
let mut data = Vec::new();
self.write(&mut data);
buf.write_all(&data)
@ -284,7 +284,7 @@ impl AzaleaWrite for simdnbt::owned::NbtTag {
}
impl AzaleaWrite for simdnbt::owned::NbtCompound {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
let mut data = Vec::new();
simdnbt::owned::NbtTag::Compound(self.clone()).write(&mut data);
buf.write_all(&data)
@ -292,7 +292,7 @@ impl AzaleaWrite for simdnbt::owned::NbtCompound {
}
impl AzaleaWrite for simdnbt::owned::Nbt {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
let mut data = Vec::new();
self.write_unnamed(&mut data);
buf.write_all(&data)
@ -303,20 +303,20 @@ impl<T> AzaleaWrite for Box<T>
where
T: AzaleaWrite,
{
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
T::azalea_write(&**self, buf)
}
}
impl<A: AzaleaWrite, B: AzaleaWrite> AzaleaWrite for (A, B) {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
self.0.azalea_write(buf)?;
self.1.azalea_write(buf)
}
}
impl<T: AzaleaWrite> AzaleaWrite for Arc<T> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
T::azalea_write(&**self, buf)
}
}

View file

@ -13,11 +13,9 @@ azalea-buf = ["dep:azalea-buf", "simdnbt"]
numbers = ["dep:azalea-registry", "dep:simdnbt"]
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "0.11.0", optional = true, features = [
"serde_json",
] }
azalea-language = { path = "../azalea-language", version = "0.12.0" }
azalea-registry = { path = "../azalea-registry", version = "0.12.0", optional = true }
azalea-buf = { workspace = true, optional = true, features = ["serde_json"] }
azalea-language.workspace = true
azalea-registry = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
simdnbt = { workspace = true, optional = true }

View file

@ -1,10 +1,16 @@
use std::{fmt::Display, sync::LazyLock};
#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
use std::io::{self, Cursor, Write};
use std::{
fmt::{self, Display},
sync::LazyLock,
};
#[cfg(feature = "azalea-buf")]
use azalea_buf::{AzaleaRead, AzaleaWrite, BufReadError};
use serde::{Deserialize, Deserializer, Serialize, de};
#[cfg(feature = "simdnbt")]
use simdnbt::{Deserialize as _, FromNbtTag as _, Serialize as _};
#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
use tracing::{debug, trace, warn};
use crate::{
@ -67,6 +73,105 @@ impl FormattedText {
}
}
/// Render all components into a single `String`, using your custom
/// closures to drive styling, text transformation, and final cleanup.
///
/// # Type params
/// - `F`: `(running, component, default) -> (prefix, suffix)` for
/// per-component styling
/// - `S`: `&str -> String` for text tweaks (escaping, mapping, etc.)
/// - `C`: `&final_running_style -> String` for any trailing cleanup
///
/// # Args
/// - `style_formatter`: how to open/close each components style
/// - `text_formatter`: how to turn raw text into output text
/// - `cleanup_formatter`: emit after all components (e.g. reset codes)
/// - `default_style`: where to reset when a components `reset` is true
///
/// # Example
/// ```rust
/// use azalea_chat::{FormattedText, DEFAULT_STYLE};
/// use serde::de::Deserialize;
///
/// let component = FormattedText::deserialize(&serde_json::json!({
/// "text": "Hello, world!",
/// "color": "red",
/// })).unwrap();
///
/// let ansi = component.to_custom_format(
/// |running, new, default| (running.compare_ansi(new, default), String::new()),
/// |text| text.to_string(),
/// |style| {
/// if !style.is_empty() {
/// "\u{1b}[m".to_string()
/// } else {
/// String::new()
/// }
/// },
/// &DEFAULT_STYLE,
/// );
/// println!("{}", ansi);
/// ```
pub fn to_custom_format<F, S, C>(
&self,
mut style_formatter: F,
mut text_formatter: S,
mut cleanup_formatter: C,
default_style: &Style,
) -> String
where
F: FnMut(&Style, &Style, &Style) -> (String, String),
S: FnMut(&str) -> String,
C: FnMut(&Style) -> String,
{
let mut output = String::new();
let mut running_style = Style::default();
for component in self.clone().into_iter() {
let component_text = match &component {
Self::Text(c) => c.text.to_string(),
Self::Translatable(c) => match c.read() {
Ok(c) => c.to_string(),
Err(_) => c.key.to_string(),
},
};
let component_style = &component.get_base().style;
let formatted_style = style_formatter(&running_style, component_style, default_style);
let formatted_text = text_formatter(&component_text);
output.push_str(&formatted_style.0);
output.push_str(&formatted_text);
output.push_str(&formatted_style.1);
// Reset running style if required
if component_style.reset {
running_style = default_style.clone();
} else {
running_style.apply(component_style);
}
}
output.push_str(&cleanup_formatter(&running_style));
output
}
/// Convert this component into an
/// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code).
///
/// This is the same as [`FormattedText::to_ansi`], but you can specify a
/// default [`Style`] to use.
pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
self.to_custom_format(
|running, new, default| (running.compare_ansi(new, default), "".to_owned()),
|text| text.to_string(),
|style| if !style.is_empty() { "\u{1b}[m" } else { "" }.to_string(),
default_style,
)
}
/// Convert this component into an
/// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code), so you
/// can print it to your terminal and get styling.
@ -89,41 +194,30 @@ impl FormattedText {
/// println!("{}", component.to_ansi());
/// ```
pub fn to_ansi(&self) -> String {
// default the default_style to white if it's not set
self.to_ansi_with_custom_style(&DEFAULT_STYLE)
}
/// Convert this component into an
/// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code).
///
/// This is the same as [`FormattedText::to_ansi`], but you can specify a
/// default [`Style`] to use.
pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
// this contains the final string will all the ansi escape codes
let mut built_string = String::new();
// this style will update as we visit components
let mut running_style = Style::default();
for component in self.clone().into_iter() {
let component_text = match &component {
Self::Text(c) => c.text.to_string(),
Self::Translatable(c) => c.to_string(),
};
let component_style = &component.get_base().style;
let ansi_text = running_style.compare_ansi(component_style, default_style);
built_string.push_str(&ansi_text);
built_string.push_str(&component_text);
running_style.apply(component_style);
}
if !running_style.is_empty() {
built_string.push_str("\u{1b}[m");
}
built_string
pub fn to_html(&self) -> String {
self.to_custom_format(
|running, new, _| {
(
format!(
"<span style=\"{}\">",
running.merged_with(new).get_html_style()
),
"</span>".to_owned(),
)
},
|text| {
text.replace("&", "&amp;")
.replace("<", "&lt;")
// usually unnecessary but good for compatibility
.replace(">", "&gt;")
.replace("\n", "<br>")
},
|_| "".to_string(),
&DEFAULT_STYLE,
)
}
}
@ -181,13 +275,12 @@ impl<'de> Deserialize<'de> for FormattedText {
// string to with_array otherwise add the component
// to the array
let c = FormattedText::deserialize(item).map_err(de::Error::custom)?;
if let FormattedText::Text(text_component) = c {
if text_component.base.siblings.is_empty()
&& text_component.base.style.is_empty()
{
with_array.push(StringOrComponent::String(text_component.text));
continue;
}
if let FormattedText::Text(text_component) = c
&& text_component.base.siblings.is_empty()
&& text_component.base.style.is_empty()
{
with_array.push(StringOrComponent::String(text_component.text));
continue;
}
with_array.push(StringOrComponent::FormattedText(
FormattedText::deserialize(item).map_err(de::Error::custom)?,
@ -344,6 +437,10 @@ impl FormattedText {
for item in with {
with_array.push(StringOrComponent::String(item.to_string()));
}
} else if let Some(with) = with_list.ints() {
for item in with {
with_array.push(StringOrComponent::String(item.to_string()));
}
} else if let Some(with) = with_list.compounds() {
for item in with {
// if it's a string component with no styling and no siblings,
@ -377,13 +474,12 @@ impl FormattedText {
with_array.push(StringOrComponent::String("?".to_string()));
}
} else if let Some(c) = FormattedText::from_nbt_compound(item) {
if let FormattedText::Text(text_component) = c {
if text_component.base.siblings.is_empty()
&& text_component.base.style.is_empty()
{
with_array.push(StringOrComponent::String(text_component.text));
continue;
}
if let FormattedText::Text(text_component) = c
&& text_component.base.siblings.is_empty()
&& text_component.base.style.is_empty()
{
with_array.push(StringOrComponent::String(text_component.text));
continue;
}
with_array.push(StringOrComponent::FormattedText(
FormattedText::from_nbt_compound(item)?,
@ -441,8 +537,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)
}
@ -455,10 +552,9 @@ impl From<&simdnbt::Mutf8Str> for FormattedText {
}
}
#[cfg(feature = "azalea-buf")]
#[cfg(feature = "simdnbt")]
#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
impl AzaleaRead for FormattedText {
fn azalea_read(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, BufReadError> {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let nbt = simdnbt::borrow::read_optional_tag(buf)?;
match nbt {
Some(nbt) => FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
@ -469,10 +565,9 @@ impl AzaleaRead for FormattedText {
}
}
#[cfg(feature = "azalea-buf")]
#[cfg(feature = "simdnbt")]
#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
impl AzaleaWrite for FormattedText {
fn azalea_write(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
let mut out = Vec::new();
simdnbt::owned::BaseNbt::write_unnamed(&(self.clone().to_compound().into()), &mut out);
buf.write_all(&out)
@ -494,7 +589,7 @@ impl From<&str> for FormattedText {
}
impl Display for FormattedText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FormattedText::Text(c) => c.fmt(f),
FormattedText::Translatable(c) => c.fmt(f),

View file

@ -1,26 +0,0 @@
enum ClickAction {
OPEN_URL = Action::new("open_url", true),
OPEN_FILE = Action::new("open_file", false),
RUN_COMMAND = Action::new("run_command", true),
SUGGEST_COMMAND = Action::new("suggest_command", true),
CHANGE_PAGE = Action::new("change_page", true),
COPY_TO_CLIPBOARD = Action::new("copy_to_clipboard", true),
}
struct ClickAction {
pub name: String,
pub allow_from_server: bool,
}
impl ClickAction {
fn new(name: &str, allow_from_server: bool) -> Self {
Self {
name: name.to_string(),
allow_from_server,
}
}
}
struct ClickEvent {
action: ClickAction,
}

View file

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

View file

@ -1,7 +1,7 @@
//! Contains a few ways to style numbers. At the time of writing, Minecraft only
//! uses this for rendering scoreboard objectives.
use std::io::{Cursor, Write};
use std::io::{self, Cursor, Write};
#[cfg(feature = "azalea-buf")]
use azalea_buf::{AzaleaRead, AzaleaWrite};
@ -35,7 +35,7 @@ impl AzaleaRead for NumberFormat {
#[cfg(feature = "azalea-buf")]
impl AzaleaWrite for NumberFormat {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
match self {
NumberFormat::Blank => NumberFormatKind::Blank.azalea_write(buf)?,
NumberFormat::Styled { style } => {

View file

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

View file

@ -1,8 +1,12 @@
use std::fmt::Display;
use std::fmt::{self, 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;
@ -125,7 +142,7 @@ impl TextComponent {
}
impl Display for TextComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// this contains the final string will all the ansi escape codes
for component in FormattedText::Text(self.clone()).into_iter() {
let component_text = match &component {
@ -146,7 +163,7 @@ mod tests {
use crate::style::Ansi;
#[test]
fn test_hypixel_motd() {
fn test_hypixel_motd_ansi() {
let component =
TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string())
.get();
@ -163,6 +180,39 @@ mod tests {
);
}
#[test]
fn test_hypixel_motd_html() {
let component =
TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string())
.get();
assert_eq!(
component.to_html(),
format!(
"{GREEN}Hypixel Network {END_SPAN}{RED}[1.8-1.18]<br>{END_SPAN}{BOLD_AQUA}HAPPY HOLIDAYS{END_SPAN}",
END_SPAN = "</span>",
GREEN = "<span style=\"color:#55FF55;\">",
RED = "<span style=\"color:#FF5555;\">",
BOLD_AQUA = "<span style=\"color:#55FFFF;font-weight:bold;\">",
)
);
}
#[test]
fn test_xss_html() {
let component = TextComponent::new("§a<b>&\n§b</b>".to_string()).get();
assert_eq!(
component.to_html(),
format!(
"{GREEN}&lt;b&gt;&amp;<br>{END_SPAN}{AQUA}&lt;/b&gt;{END_SPAN}",
END_SPAN = "</span>",
GREEN = "<span style=\"color:#55FF55;\">",
AQUA = "<span style=\"color:#55FFFF;\">",
)
);
}
#[test]
fn test_legacy_color_code_to_component() {
let component = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_string()).get();
@ -180,4 +230,17 @@ mod tests {
)
);
}
#[test]
fn test_legacy_color_code_with_rgb() {
let component = TextComponent::new("§#Ff0000This is a test message".to_string()).get();
assert_eq!(
component.to_ansi(),
format!(
"{RED}This is a test message{RESET}",
RED = Ansi::rgb(0xff0000),
RESET = Ansi::RESET
)
);
}
}

View file

@ -1,4 +1,4 @@
use std::fmt::{self, Display, Formatter};
use std::fmt::{self, Display};
use serde::{__private::ser::FlatMapSerializer, Serialize, Serializer, ser::SerializeMap};
#[cfg(feature = "simdnbt")]
@ -189,7 +189,7 @@ impl TranslatableComponent {
}
impl Display for TranslatableComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// this contains the final string will all the ansi escape codes
for component in FormattedText::Translatable(self.clone()).into_iter() {
let component_text = match &component {
@ -208,7 +208,7 @@ impl Display for TranslatableComponent {
}
impl Display for StringOrComponent {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
StringOrComponent::String(s) => write!(f, "{s}"),
StringOrComponent::FormattedText(c) => write!(f, "{c}"),

View file

@ -7,23 +7,25 @@ license.workspace = true
repository.workspace = true
[dependencies]
azalea-auth = { path = "../azalea-auth", version = "0.12.0" }
azalea-block = { path = "../azalea-block", version = "0.12.0" }
azalea-buf = { path = "../azalea-buf", version = "0.12.0" }
azalea-chat = { path = "../azalea-chat", version = "0.12.0" }
azalea-core = { path = "../azalea-core", version = "0.12.0" }
azalea-crypto = { path = "../azalea-crypto", version = "0.12.0" }
azalea-entity = { path = "../azalea-entity", version = "0.12.0" }
azalea-inventory = { path = "../azalea-inventory", version = "0.12.0" }
azalea-physics = { path = "../azalea-physics", version = "0.12.0" }
azalea-protocol = { path = "../azalea-protocol", version = "0.12.0" }
azalea-registry = { path = "../azalea-registry", version = "0.12.0" }
azalea-world = { path = "../azalea-world", version = "0.12.0" }
async-compat.workspace = true
azalea-auth.workspace = true
azalea-block.workspace = true
azalea-buf.workspace = true
azalea-chat.workspace = true
azalea-core.workspace = true
azalea-crypto.workspace = true
azalea-entity.workspace = true
azalea-inventory.workspace = true
azalea-physics.workspace = true
azalea-protocol.workspace = true
azalea-registry.workspace = true
azalea-world.workspace = true
bevy_app.workspace = true
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
@ -40,6 +42,7 @@ uuid.workspace = true
anyhow.workspace = true
[features]
default = ["log"]
default = ["log", "packet-event"]
# enables bevy_log::LogPlugin by default
log = ["bevy_log"]
packet-event = []

View file

@ -2,8 +2,10 @@
use std::sync::Arc;
use azalea_auth::AccessTokenResponse;
use azalea_auth::certs::{Certificates, FetchCertificatesError};
use azalea_auth::{
AccessTokenResponse,
certs::{Certificates, FetchCertificatesError},
};
use bevy_ecs::component::Component;
use parking_lot::Mutex;
use thiserror::Error;
@ -15,7 +17,7 @@ use uuid::Uuid;
/// To join a server using this account, use [`Client::join`] or
/// [`azalea::ClientBuilder`].
///
/// Note that this is also a component that our clients have.
/// This is also an ECS component that is present on our client entities.
///
/// # Examples
///
@ -53,7 +55,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 +84,7 @@ impl Account {
account_opts: AccountOpts::Offline {
username: username.to_string(),
},
certs: None,
certs: Arc::new(Mutex::new(None)),
}
}
@ -127,7 +129,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 +196,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 +262,7 @@ impl Account {
.lock()
.clone();
let certs = azalea_auth::certs::fetch_certificates(&access_token).await?;
self.certs = Some(certs);
*self.certs.lock() = Some(certs);
Ok(())
}

View file

@ -1,94 +1,65 @@
use std::{
collections::HashMap,
fmt::Debug,
io,
mem,
net::SocketAddr,
sync::Arc,
thread,
time::{Duration, Instant},
};
use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
use azalea_chat::FormattedText;
use azalea_auth::game_profile::GameProfile;
use azalea_core::{
data_registry::ResolvableDataRegistry, position::Vec3, resource_location::ResourceLocation,
tick::GameTick,
};
use azalea_entity::{
EntityPlugin, EntityUpdateSet, EyeHeight, LocalEntity, Position,
EntityUpdateSet, EyeHeight, Position,
indexing::{EntityIdIndex, EntityUuidIndex},
metadata::Health,
};
use azalea_physics::PhysicsPlugin;
use azalea_protocol::{
ServerAddress,
common::client_information::ClientInformation,
connect::{Connection, ConnectionError, Proxy},
connect::Proxy,
packets::{
self, ClientIntention, ConnectionProtocol, PROTOCOL_VERSION, Packet,
config::{ClientboundConfigPacket, ServerboundConfigPacket},
game::ServerboundGamePacket,
handshake::{
ClientboundHandshakePacket, ServerboundHandshakePacket,
s_intention::ServerboundIntention,
},
login::{
ClientboundLoginPacket, s_hello::ServerboundHello, s_key::ServerboundKey,
s_login_acknowledged::ServerboundLoginAcknowledged,
},
Packet,
game::{self, ServerboundGamePacket},
},
resolver,
};
use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
use bevy_app::{App, Plugin, PluginGroup, PluginGroupBuilder, 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::{ResMut, Resource},
world::World,
prelude::*,
schedule::{InternedScheduleLabel, LogLevel, ScheduleBuildSettings},
};
use bevy_time::TimePlugin;
use derive_more::Deref;
use parking_lot::{Mutex, RwLock};
use simdnbt::owned::NbtCompound;
use thiserror::Error;
use tokio::{
sync::{
broadcast,
mpsc::{self, error::TrySendError},
},
sync::mpsc::{self},
time,
};
use tracing::{debug, error, info};
use tracing::{debug, error, info, warn};
use uuid::Uuid;
use crate::{
Account, PlayerInfo,
attack::{self, AttackPlugin},
brand::BrandPlugin,
chat::ChatPlugin,
chunks::{ChunkBatchInfo, ChunksPlugin},
disconnect::{DisconnectEvent, DisconnectPlugin},
events::{Event, EventsPlugin, LocalPlayerEvents},
interact::{CurrentSequenceNumber, InteractPlugin},
inventory::{Inventory, InventoryPlugin},
local_player::{
GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList,
},
mining::{self, MiningPlugin},
movement::{LastSentLookDirection, MovementPlugin, PhysicsState},
packet::{
PacketPlugin,
login::{self, InLoginState, LoginSendPacketQueue},
},
player::retroactively_add_game_profile_component,
pong::PongPlugin,
raw_connection::RawConnection,
respawn::RespawnPlugin,
task_pool::TaskPoolPlugin,
tick_end::TickEndPlugin,
Account, DefaultPlugins,
attack::{self},
block_update::QueuedServerBlockUpdates,
chunks::ChunkBatchInfo,
connection::RawConnection,
disconnect::DisconnectEvent,
events::Event,
interact::BlockStatePredictionHandler,
inventory::Inventory,
join::{ConnectOpts, StartJoinServerEvent},
local_player::{Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList},
mining::{self},
movement::{LastSentLookDirection, PhysicsState},
packet::game::SendPacketEvent,
player::{GameProfileComponent, PlayerInfo, retroactively_add_game_profile_component},
};
/// `Client` has the things that a user interacting with the library will want.
@ -102,15 +73,6 @@ use crate::{
/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html
#[derive(Clone)]
pub struct Client {
/// The [`GameProfile`] for our client. This contains your username, UUID,
/// and skin data.
///
/// This is immutable; the server cannot change it. To get the username and
/// skin the server chose for you, get your player from the [`TabList`]
/// component.
///
/// This as also available from the ECS as [`GameProfileComponent`].
pub profile: GameProfile,
/// The entity for this client in the ECS.
pub entity: Entity,
@ -118,9 +80,6 @@ pub struct Client {
/// directly. Note that if you're using a shared world (i.e. a swarm), this
/// will contain all entities in all worlds.
pub ecs: Arc<Mutex<World>>,
/// Use this to force the client to run the schedule outside of a tick.
pub run_schedule_sender: mpsc::Sender<()>,
}
/// An error that happened while joining the server.
@ -128,60 +87,44 @@ pub struct Client {
pub enum JoinError {
#[error("{0}")]
Resolver(#[from] resolver::ResolverError),
#[error("{0}")]
Connection(#[from] ConnectionError),
#[error("{0}")]
ReadPacket(#[from] Box<azalea_protocol::read::ReadPacketError>),
#[error("{0}")]
Io(#[from] io::Error),
#[error("{0}")]
SessionServer(#[from] azalea_auth::sessionserver::ClientSessionServerError),
#[error("The given address could not be parsed into a ServerAddress")]
InvalidAddress,
#[error("Couldn't refresh access token: {0}")]
Auth(#[from] azalea_auth::AuthError),
#[error("Disconnected: {reason}")]
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 run_schedule_sender: mpsc::Sender<()>,
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> {
// An event that causes the schedule to run. This is only used internally.
let (run_schedule_sender, run_schedule_receiver) = mpsc::channel(1);
) -> StartClientOpts {
let mut app = App::new();
app.add_plugins(DefaultPlugins);
let ecs_lock = start_ecs_runner(app, run_schedule_receiver, run_schedule_sender.clone());
let (ecs_lock, start_running_systems) = start_ecs_runner(app.main_mut());
start_running_systems();
Self {
ecs_lock,
account,
address,
resolved_address,
proxy: None,
run_schedule_sender,
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
}
}
@ -191,20 +134,12 @@ impl Client {
/// World, and schedule runner function.
/// You should only use this if you want to change these fields from the
/// defaults, otherwise use [`Client::join`].
pub fn new(
profile: GameProfile,
entity: Entity,
ecs: Arc<Mutex<World>>,
run_schedule_sender: mpsc::Sender<()>,
) -> Self {
pub fn new(entity: Entity, ecs: Arc<Mutex<World>>) -> Self {
Self {
profile,
// default our id to 0, it'll be set later
entity,
ecs,
run_schedule_sender,
}
}
@ -217,19 +152,19 @@ impl Client {
/// # Examples
///
/// ```rust,no_run
/// use azalea_client::{Client, Account};
/// use azalea_client::{Account, Client};
///
/// #[tokio::main]
/// #[tokio::main(flavor = "current_thread")]
/// 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)?;
@ -238,16 +173,16 @@ impl Client {
let client = Self::start_client(StartClientOpts::new(
account,
&address,
&resolved_address,
address,
resolved_address,
Some(tx),
))
.await?;
.await;
Ok((client, rx))
}
pub async fn join_with_proxy(
account: &Account,
account: Account,
address: impl TryInto<ServerAddress>,
proxy: Proxy,
) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
@ -256,9 +191,9 @@ 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?;
.await;
Ok((client, rx))
}
@ -268,266 +203,36 @@ impl Client {
StartClientOpts {
ecs_lock,
account,
address,
resolved_address,
proxy,
run_schedule_sender,
connect_opts,
event_sender,
}: 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();
}: StartClientOpts,
) -> Self {
// 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::<Entity>();
// add the Account to the entity now so plugins can access it earlier
ecs.entity_mut(entity).insert(account.to_owned());
ecs_lock.lock().send_event(StartJoinServerEvent {
account,
connect_opts,
event_sender,
start_join_callback_tx: Some(start_join_callback_tx),
});
entity
};
let conn = if let Some(proxy) = proxy {
Connection::new_with_proxy(resolved_address, proxy).await?
} else {
Connection::new(resolved_address).await?
};
let (conn, game_profile) =
Self::handshake(ecs_lock.clone(), entity, conn, account, address).await?;
// note that we send the proper packets in
// crate::configuration::handle_in_configuration_state
let (read_conn, write_conn) = conn.into_split();
let (read_conn, write_conn) = (read_conn.raw, write_conn.raw);
// we did the handshake, so now we're connected to the server
let mut ecs = ecs_lock.lock();
// we got the ConfigurationConnection, so the client is now connected :)
let client = Client::new(
game_profile.clone(),
entity,
ecs_lock.clone(),
run_schedule_sender.clone(),
let entity = start_join_callback_rx.recv().await.expect(
"start_join_callback should not be dropped before sending a message, this is a bug in Azalea",
);
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(
run_schedule_sender,
ConnectionProtocol::Configuration,
read_conn,
write_conn,
),
game_profile: GameProfileComponent(game_profile),
client_information: crate::ClientInformation::default(),
instance_holder,
metadata: azalea_entity::metadata::PlayerMetadataBundle::default(),
},
InConfigState,
// this component is never removed
LocalEntity,
));
if let Some(event_sender) = event_sender {
// this is optional so we don't leak memory in case the user
entity.insert(LocalPlayerEvents(event_sender));
}
Ok(client)
}
/// Do a handshake with the server and get to the game state from the
/// initial handshake state.
///
/// This will also automatically refresh the account's access token if
/// it's expired.
pub async fn handshake(
ecs_lock: Arc<Mutex<World>>,
entity: Entity,
mut conn: Connection<ClientboundHandshakePacket, ServerboundHandshakePacket>,
account: &Account,
address: &ServerAddress,
) -> Result<
(
Connection<ClientboundConfigPacket, ServerboundConfigPacket>,
GameProfile,
),
JoinError,
> {
// handshake
conn.write(ServerboundIntention {
protocol_version: PROTOCOL_VERSION,
hostname: address.host.clone(),
port: address.port,
intention: ClientIntention::Login,
})
.await?;
let mut conn = conn.login();
// this makes it so plugins can send an `SendLoginPacketEvent` event to the ecs
// and we'll send it to the server
let (ecs_packets_tx, mut ecs_packets_rx) = mpsc::unbounded_channel();
ecs_lock.lock().entity_mut(entity).insert((
LoginSendPacketQueue { tx: ecs_packets_tx },
crate::packet::login::IgnoreQueryIds::default(),
InLoginState,
));
// login
conn.write(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(),
})
.await?;
let (conn, profile) = loop {
let packet = tokio::select! {
packet = conn.read() => packet?,
Some(packet) = ecs_packets_rx.recv() => {
// write this packet to the server
conn.write(packet).await?;
continue;
}
};
ecs_lock.lock().send_event(login::LoginPacketEvent {
entity,
packet: Arc::new(packet.clone()),
});
match packet {
ClientboundLoginPacket::Hello(p) => {
debug!("Got encryption request");
let Ok(e) = azalea_crypto::encrypt(&p.public_key, &p.challenge) else {
error!("Failed to encrypt the challenge from the server for {p:?}");
continue;
};
if let Some(access_token) = &account.access_token {
// keep track of the number of times we tried
// authenticating so we can give up after too many
let mut attempts: usize = 1;
while let Err(e) = {
let access_token = access_token.lock().clone();
conn.authenticate(
&access_token,
&account
.uuid
.expect("Uuid must be present if access token is present."),
e.secret_key,
&p,
)
.await
} {
if attempts >= 2 {
// if this is the second attempt and we failed
// both times, give up
return Err(e.into());
}
if matches!(
e,
ClientSessionServerError::InvalidSession
| ClientSessionServerError::ForbiddenOperation
) {
// uh oh, we got an invalid session and have
// to reauthenticate now
account.refresh().await?;
} else {
return Err(e.into());
}
attempts += 1;
}
}
conn.write(ServerboundKey {
key_bytes: e.encrypted_public_key,
encrypted_challenge: e.encrypted_challenge,
})
.await?;
conn.set_encryption_key(e.secret_key);
}
ClientboundLoginPacket::LoginCompression(p) => {
debug!("Got compression request {:?}", p.compression_threshold);
conn.set_compression_threshold(p.compression_threshold);
}
ClientboundLoginPacket::LoginFinished(p) => {
debug!(
"Got profile {:?}. handshake is finished and we're now switching to the configuration state",
p.game_profile
);
conn.write(ServerboundLoginAcknowledged {}).await?;
break (conn.config(), p.game_profile);
}
ClientboundLoginPacket::LoginDisconnect(p) => {
debug!("Got disconnect {:?}", p);
return Err(JoinError::Disconnect { reason: p.reason });
}
ClientboundLoginPacket::CustomQuery(p) => {
debug!("Got custom query {:?}", p);
// replying to custom query is done in
// packet::login::process_packet_events
}
ClientboundLoginPacket::CookieRequest(p) => {
debug!("Got cookie request {:?}", p);
conn.write(packets::login::ServerboundCookieResponse {
key: p.key,
// cookies aren't implemented
payload: None,
})
.await?;
}
}
};
ecs_lock
.lock()
.entity_mut(entity)
.remove::<login::IgnoreQueryIds>()
.remove::<LoginSendPacketQueue>()
.remove::<InLoginState>();
Ok((conn, profile))
Client::new(entity, ecs_lock)
}
/// Write a packet directly to the server.
pub fn write_packet(
&self,
packet: impl Packet<ServerboundGamePacket>,
) -> Result<(), crate::raw_connection::WritePacketError> {
pub fn write_packet(&self, packet: impl Packet<ServerboundGamePacket>) {
let packet = packet.into_variant();
self.raw_connection_mut(&mut self.ecs.lock())
.write_packet(packet)
self.ecs
.lock()
.commands()
.trigger(SendPacketEvent::new(self.entity, packet));
}
/// Disconnect this client from the server by ending all tasks.
@ -618,7 +323,7 @@ impl Client {
/// This will panic if the component doesn't exist on the client.
///
/// ```
/// # use azalea_client::{Client, Hunger};
/// # use azalea_client::{Client, local_player::Hunger};
/// # fn example(bot: &Client) {
/// let hunger = bot.map_component::<Hunger, _>(|h| h.food);
/// # }
@ -634,17 +339,10 @@ impl Client {
/// Similar to [`Self::get_component`], but doesn't clone the component
/// since it's passed as a reference. [`Self::ecs`] will remain locked
/// while the callback is being run.
///
/// ```
/// # use azalea_client::{Client, mining::Mining};
/// # fn example(bot: &Client) {
/// let is_mining = bot.map_get_component::<Mining, _>(|m| m.is_some());
/// # }
/// ```
pub fn map_get_component<T: Component, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R {
pub fn map_get_component<T: Component, R>(&self, f: impl FnOnce(&T) -> R) -> Option<R> {
let mut ecs = self.ecs.lock();
let value = self.query::<Option<&T>>(&mut ecs);
f(value)
value.map(f)
}
/// Get an `RwLock` with a reference to our (potentially shared) world.
@ -690,14 +388,11 @@ impl Client {
/// view_distance: 2,
/// ..Default::default()
/// })
/// .await?;
/// .await;
/// # Ok(())
/// # }
/// ```
pub async fn set_client_information(
&self,
client_information: ClientInformation,
) -> Result<(), crate::raw_connection::WritePacketError> {
pub async fn set_client_information(&self, client_information: ClientInformation) {
{
let mut ecs = self.ecs.lock();
let mut client_information_mut = self.query::<&mut ClientInformation>(&mut ecs);
@ -709,10 +404,10 @@ impl Client {
"Sending client information (already logged in): {:?}",
client_information
);
self.write_packet(azalea_protocol::packets::game::s_client_information::ServerboundClientInformation { information: client_information.clone() })?;
self.write_packet(game::s_client_information::ServerboundClientInformation {
client_information,
});
}
Ok(())
}
}
@ -760,14 +455,14 @@ impl Client {
/// This is a shortcut for
/// `bot.component::<GameProfileComponent>().name.to_owned()`.
pub fn username(&self) -> String {
self.component::<GameProfileComponent>().name.to_owned()
self.profile().name.to_owned()
}
/// Get the Minecraft UUID of this client.
///
/// This is a shortcut for `bot.component::<GameProfileComponent>().uuid`.
pub fn uuid(&self) -> Uuid {
self.component::<GameProfileComponent>().uuid
self.profile().uuid
}
/// Get a map of player UUIDs to their information in the tab list.
@ -777,6 +472,19 @@ impl Client {
(*self.component::<TabList>()).clone()
}
/// Returns the [`GameProfile`] for our client. This contains your username,
/// UUID, and skin data.
///
/// These values are set by the server upon login, which means they might
/// not match up with your actual game profile. Also, note that the username
/// and skin that gets displayed in-game will actually be the ones from
/// the tab list, which you can get from [`Self::tab_list`].
///
/// This as also available from the ECS as [`GameProfileComponent`].
pub fn profile(&self) -> GameProfile {
(*self.component::<GameProfileComponent>()).clone()
}
/// A convenience function to get the Minecraft Uuid of a player by their
/// username, if they're present in the tab list.
///
@ -857,16 +565,14 @@ impl Client {
}
}
/// The bundle of components that's shared when we're either in the
/// `configuration` or `game` state.
/// A bundle of components that's inserted right when we switch to the `login`
/// state and stay present on our clients until we disconnect.
///
/// For the components that are only present in the `game` state, see
/// [`JoinedClientBundle`].
#[derive(Bundle)]
pub struct LocalPlayerBundle {
pub raw_connection: RawConnection,
pub game_profile: GameProfileComponent,
pub client_information: ClientInformation,
pub instance_holder: InstanceHolder,
pub metadata: azalea_entity::metadata::PlayerMetadataBundle,
@ -881,7 +587,8 @@ pub struct JoinedClientBundle {
pub physics_state: PhysicsState,
pub inventory: Inventory,
pub tab_list: TabList,
pub current_sequence_number: CurrentSequenceNumber,
pub block_state_prediction_handler: BlockStatePredictionHandler,
pub queued_server_block_updates: QueuedServerBlockUpdates,
pub last_sent_direction: LastSentLookDirection,
pub abilities: PlayerAbilities,
pub permission_level: PermissionLevel,
@ -912,7 +619,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>()
@ -920,16 +629,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,
run_schedule_receiver: mpsc::Receiver<()>,
run_schedule_sender: mpsc::Sender<()>,
) -> 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 {
@ -947,40 +654,60 @@ pub fn start_ecs_runner(
// 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(),
run_schedule_receiver,
));
tokio::spawn(tick_run_schedule_loop(run_schedule_sender));
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,
mut run_schedule_receiver: mpsc::Receiver<()>,
) {
async fn run_schedule_loop(ecs: Arc<Mutex<World>>, outer_schedule_label: InternedScheduleLabel) {
let mut last_update: Option<Instant> = None;
let mut last_tick: Option<Instant> = None;
// azalea runs the Update schedule at most 60 times per second to simulate
// framerate. unlike vanilla though, we also only handle packets during Updates
// due to everything running in ecs systems.
const UPDATE_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 60);
// minecraft runs at 20 tps
const GAME_TICK_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 20);
loop {
// whenever we get an event from run_schedule_receiver, run the schedule
run_schedule_receiver.recv().await;
// sleep until the next update if necessary
let now = Instant::now();
if let Some(last_update) = last_update {
let elapsed = now.duration_since(last_update);
if elapsed < UPDATE_DURATION_TARGET {
time::sleep(UPDATE_DURATION_TARGET - elapsed).await;
}
}
last_update = Some(now);
let mut ecs = ecs.lock();
// if last tick is None or more than 50ms ago, run the GameTick schedule
ecs.run_schedule(outer_schedule_label);
if last_tick
.map(|last_tick| last_tick.elapsed() > Duration::from_millis(50))
.map(|last_tick| last_tick.elapsed() > GAME_TICK_DURATION_TARGET)
.unwrap_or(true)
{
if let Some(last_tick) = &mut last_tick {
*last_tick += Duration::from_millis(50);
*last_tick += GAME_TICK_DURATION_TARGET;
// if we're more than 10 ticks behind, set last_tick to now.
// vanilla doesn't do it in exactly the same way but it shouldn't really matter
if (now - *last_tick) > GAME_TICK_DURATION_TARGET * 10 {
warn!(
"GameTick is more than 10 ticks behind, skipping ticks so we don't have to burst too much"
);
*last_tick = now;
}
} else {
last_tick = Some(Instant::now());
last_tick = Some(now);
}
ecs.run_schedule(GameTick);
}
@ -989,56 +716,6 @@ async fn run_schedule_loop(
}
}
/// Send an event to run the schedule every 50 milliseconds. It will stop when
/// the receiver is dropped.
pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::Sender<()>) {
let mut game_tick_interval = time::interval(Duration::from_millis(50));
// TODO: Minecraft bursts up to 10 ticks and then skips, we should too
game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst);
loop {
game_tick_interval.tick().await;
if let Err(TrySendError::Closed(())) = run_schedule_sender.try_send(()) {
error!("tick_run_schedule_loop failed because run_schedule_sender was closed");
// the sender is closed so end the task
return;
}
}
}
/// A resource that contains a [`broadcast::Sender`] that will be sent every
/// Minecraft tick.
///
/// This is useful for running code every schedule from async user code.
///
/// ```
/// use azalea_client::TickBroadcast;
/// # async fn example(client: azalea_client::Client) {
/// let mut receiver = {
/// let ecs = client.ecs.lock();
/// let tick_broadcast = ecs.resource::<TickBroadcast>();
/// tick_broadcast.subscribe()
/// };
/// while receiver.recv().await.is_ok() {
/// // do something
/// }
/// # }
/// ```
#[derive(Resource, Deref)]
pub struct TickBroadcast(broadcast::Sender<()>);
pub fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
let _ = tick_broadcast.0.send(());
}
/// A plugin that makes the [`RanScheduleBroadcast`] resource available.
pub struct TickBroadcastPlugin;
impl Plugin for TickBroadcastPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(TickBroadcast(broadcast::channel(1).0))
.add_systems(GameTick, send_tick_broadcast);
}
}
pub struct AmbiguityLoggerPlugin;
impl Plugin for AmbiguityLoggerPlugin {
fn build(&self, app: &mut App) {
@ -1056,40 +733,3 @@ impl Plugin for AmbiguityLoggerPlugin {
});
}
}
/// This plugin group will add all the default plugins necessary for Azalea to
/// work.
pub struct DefaultPlugins;
impl PluginGroup for DefaultPlugins {
fn build(self) -> PluginGroupBuilder {
#[allow(unused_mut)]
let mut group = PluginGroupBuilder::start::<Self>()
.add(AmbiguityLoggerPlugin)
.add(TimePlugin)
.add(PacketPlugin)
.add(AzaleaPlugin)
.add(EntityPlugin)
.add(PhysicsPlugin)
.add(EventsPlugin)
.add(TaskPoolPlugin::default())
.add(InventoryPlugin)
.add(ChatPlugin)
.add(DisconnectPlugin)
.add(MovementPlugin)
.add(InteractPlugin)
.add(RespawnPlugin)
.add(MiningPlugin)
.add(AttackPlugin)
.add(ChunksPlugin)
.add(TickEndPlugin)
.add(BrandPlugin)
.add(TickBroadcastPlugin)
.add(PongPlugin);
#[cfg(feature = "log")]
{
group = group.add(bevy_log::LogPlugin::default());
}
group
}
}

View file

@ -1,10 +1,12 @@
use std::sync::Arc;
use std::{any, sync::Arc};
use azalea_core::position::Vec3;
use azalea_entity::Position;
use azalea_world::InstanceName;
use bevy_ecs::{
component::Component,
entity::Entity,
query::QueryData,
query::{QueryFilter, ROQueryItem},
query::{QueryData, QueryFilter, ROQueryItem},
world::World,
};
use parking_lot::Mutex;
@ -29,23 +31,26 @@ impl Client {
.unwrap_or_else(|_| {
panic!(
"Our client is missing a required component {:?}",
std::any::type_name::<D>()
any::type_name::<D>()
)
})
}
/// Return a lightweight [`Entity`] for the entity that matches the given
/// predicate function.
/// Return a lightweight [`Entity`] for an arbitrary entity that matches the
/// given predicate function that is in the same [`Instance`] as the
/// client.
///
/// You can then use [`Self::entity_component`] to get components from this
/// entity.
///
/// Also see [`Self::entities_by`] which will return all entities that match
/// the predicate and sorts them by distance (unlike `entity_by`).
///
/// # Example
/// Note that this will very likely change in the future.
/// ```
/// use azalea_client::{Client, GameProfileComponent};
/// use bevy_ecs::query::With;
/// use azalea_client::{Client, player::GameProfileComponent};
/// use azalea_entity::{Position, metadata::Player};
/// use bevy_ecs::query::With;
///
/// # fn example(mut bot: Client, sender_name: String) {
/// let entity = bot.entity_by::<With<Player>, (&GameProfileComponent,)>(
@ -59,11 +64,31 @@ impl Client {
/// ```
///
/// [`Entity`]: bevy_ecs::entity::Entity
/// [`Instance`]: azalea_world::Instance
pub fn entity_by<F: QueryFilter, Q: QueryData>(
&self,
predicate: impl EntityPredicate<Q, F>,
) -> Option<Entity> {
predicate.find(self.ecs.clone())
let instance_name = self.get_component::<InstanceName>()?;
predicate.find_any(self.ecs.clone(), &instance_name)
}
/// Similar to [`Self::entity_by`] but returns a `Vec<Entity>` of all
/// entities in our instance that match the predicate.
///
/// Unlike `entity_by`, the result is sorted by distance to our client's
/// position, so the closest entity is first.
pub fn entities_by<F: QueryFilter, Q: QueryData>(
&self,
predicate: impl EntityPredicate<Q, F>,
) -> Vec<Entity> {
let Some(instance_name) = self.get_component::<InstanceName>() else {
return vec![];
};
let Some(position) = self.get_component::<Position>() else {
return vec![];
};
predicate.find_all_sorted(self.ecs.clone(), &instance_name, (&position).into())
}
/// Get a component from an entity. Note that this will return an owned type
@ -77,7 +102,7 @@ impl Client {
let components = q.get(&ecs, entity).unwrap_or_else(|_| {
panic!(
"Entity is missing a required component {:?}",
std::any::type_name::<Q>()
any::type_name::<Q>()
)
});
components.clone()
@ -95,35 +120,54 @@ impl Client {
}
pub trait EntityPredicate<Q: QueryData, Filter: QueryFilter> {
fn find(&self, ecs_lock: Arc<Mutex<World>>) -> Option<Entity>;
fn find_any(&self, ecs_lock: Arc<Mutex<World>>, instance_name: &InstanceName)
-> Option<Entity>;
fn find_all_sorted(
&self,
ecs_lock: Arc<Mutex<World>>,
instance_name: &InstanceName,
nearest_to: Vec3,
) -> Vec<Entity>;
}
impl<F, Q, Filter> EntityPredicate<Q, Filter> for F
impl<F, Q: QueryData, Filter: QueryFilter> EntityPredicate<Q, Filter> for F
where
F: Fn(&ROQueryItem<Q>) -> bool,
Q: QueryData,
Filter: QueryFilter,
{
fn find(&self, ecs_lock: Arc<Mutex<World>>) -> Option<Entity> {
fn find_any(
&self,
ecs_lock: Arc<Mutex<World>>,
instance_name: &InstanceName,
) -> Option<Entity> {
let mut ecs = ecs_lock.lock();
let mut query = ecs.query_filtered::<(Entity, Q), Filter>();
query.iter(&ecs).find(|(_, q)| (self)(q)).map(|(e, _)| e)
let mut query = ecs.query_filtered::<(Entity, &InstanceName, Q), Filter>();
query
.iter(&ecs)
.find(|(_, e_instance_name, q)| *e_instance_name == instance_name && (self)(q))
.map(|(e, _, _)| e)
}
fn find_all_sorted(
&self,
ecs_lock: Arc<Mutex<World>>,
instance_name: &InstanceName,
nearest_to: Vec3,
) -> Vec<Entity> {
let mut ecs = ecs_lock.lock();
let mut query = ecs.query_filtered::<(Entity, &InstanceName, &Position, Q), Filter>();
let mut entities = query
.iter(&ecs)
.filter(|(_, e_instance_name, _, q)| *e_instance_name == instance_name && (self)(q))
.map(|(e, _, position, _)| (e, Vec3::from(position)))
.collect::<Vec<(Entity, Vec3)>>();
entities.sort_by_cached_key(|(_, position)| {
// to_bits is fine here as long as the number is positive
position.distance_squared_to(nearest_to).to_bits()
});
entities
.into_iter()
.map(|(e, _)| e)
.collect::<Vec<Entity>>()
}
}
// impl<'a, F, Q1, Q2> EntityPredicate<'a, (Q1, Q2)> for F
// where
// F: Fn(&<Q1 as WorldQuery>::Item<'_>, &<Q2 as WorldQuery>::Item<'_>) ->
// bool, Q1: QueryFilter,
// Q2: QueryFilter,
// {
// fn find(&self, ecs: &mut Ecs) -> Option<Entity> {
// // (self)(query)
// let mut query = ecs.query_filtered::<(Entity, Q1, Q2), ()>();
// let entity = query
// .iter(ecs)
// .find(|(_, q1, q2)| (self)(q1, q2))
// .map(|(e, _, _)| e);
// entity
// }
// }

View file

@ -11,25 +11,26 @@
mod account;
mod client;
mod entity_query;
mod local_player;
pub mod local_player;
pub mod ping;
mod player;
pub mod player;
mod plugins;
pub mod raw_connection;
#[cfg(feature = "log")]
#[doc(hidden)]
pub mod test_simulation;
pub mod test_utils;
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, DefaultPlugins, InConfigState, InGameState, JoinError, JoinedClientBundle,
LocalPlayerBundle, StartClientOpts, TickBroadcast, start_ecs_runner,
Client, InConfigState, InGameState, JoinError, JoinedClientBundle, LocalPlayerBundle,
StartClientOpts, start_ecs_runner,
};
pub use events::Event;
pub use local_player::{GameProfileComponent, Hunger, InstanceHolder, TabList};
pub use movement::{
PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection,
};
pub use player::PlayerInfo;
pub use plugins::*;

View file

@ -1,6 +1,9 @@
use std::{collections::HashMap, io, sync::Arc};
use std::{
collections::HashMap,
error, io,
sync::{Arc, PoisonError},
};
use azalea_auth::game_profile::GameProfile;
use azalea_core::game_type::GameMode;
use azalea_protocol::packets::game::c_player_abilities::ClientboundPlayerAbilities;
use azalea_world::{Instance, PartialInstance};
@ -12,7 +15,7 @@ use tokio::sync::mpsc;
use tracing::error;
use uuid::Uuid;
use crate::{ClientInformation, PlayerInfo, events::Event as AzaleaEvent};
use crate::{ClientInformation, events::Event as AzaleaEvent, player::PlayerInfo};
/// A component that keeps strong references to our [`PartialInstance`] and
/// [`Instance`] for local players.
@ -36,14 +39,6 @@ pub struct InstanceHolder {
pub instance: Arc<RwLock<Instance>>,
}
/// A component only present in players that contains the [`GameProfile`] (which
/// you can use to get a player's name).
///
/// Note that it's possible for this to be missing in a player if the server
/// never sent the player info for them (though this is uncommon).
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct GameProfileComponent(pub GameProfile);
/// The gamemode of a local player. For a non-local player, you can look up the
/// player in the [`TabList`].
#[derive(Component, Clone, Debug, Copy)]
@ -51,6 +46,14 @@ pub struct LocalGameMode {
pub current: GameMode,
pub previous: Option<GameMode>,
}
impl From<GameMode> for LocalGameMode {
fn from(current: GameMode) -> Self {
LocalGameMode {
current,
previous: None,
}
}
}
/// A component that contains the abilities the player has, like flying
/// or instantly breaking blocks. This is only present on local players.
@ -88,7 +91,7 @@ pub struct PermissionLevel(pub u8);
/// tab list.
///
/// ```
/// # use azalea_client::TabList;
/// # use azalea_client::local_player::TabList;
/// # fn example(client: &azalea_client::Client) {
/// let tab_list = client.component::<TabList>();
/// println!("Online players:");
@ -144,6 +147,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)]
@ -153,13 +172,13 @@ pub enum HandlePacketError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
Other(#[from] Box<dyn error::Error + Send + Sync>),
#[error("{0}")]
Send(#[from] mpsc::error::SendError<AzaleaEvent>),
}
impl<T> From<std::sync::PoisonError<T>> for HandlePacketError {
fn from(e: std::sync::PoisonError<T>) -> Self {
impl<T> From<PoisonError<T>> for HandlePacketError {
fn from(e: PoisonError<T>) -> Self {
HandlePacketError::Poison(e.to_string())
}
}

View file

@ -3,12 +3,14 @@ use azalea_chat::FormattedText;
use azalea_core::game_type::GameMode;
use azalea_entity::indexing::EntityUuidIndex;
use bevy_ecs::{
component::Component,
event::EventReader,
system::{Commands, Res},
};
use derive_more::{Deref, DerefMut};
use uuid::Uuid;
use crate::{GameProfileComponent, packet::game::AddPlayerEvent};
use crate::packet::game::AddPlayerEvent;
/// A player in the tab list.
#[derive(Debug, Clone)]
@ -26,9 +28,17 @@ pub struct PlayerInfo {
/// The player's display name in the tab list, but only if it's different
/// from the player's normal username. Use `player_info.profile.name` to get
/// the player's actual username.
pub display_name: Option<FormattedText>,
pub display_name: Option<Box<FormattedText>>,
}
/// A component only present in players that contains the [`GameProfile`] (which
/// you can use to get a player's name).
///
/// Note that it's possible for this to be missing in a player if the server
/// never sent the player info for them (though this is uncommon).
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct GameProfileComponent(pub GameProfile);
/// Add a [`GameProfileComponent`] when an [`AddPlayerEvent`] is received.
/// Usually the `GameProfileComponent` will be added from the
/// `ClientboundGamePacket::AddPlayer` handler though.

View file

@ -1,15 +1,16 @@
use azalea_core::{game_type::GameMode, tick::GameTick};
use azalea_entity::{
Attributes, Physics,
indexing::EntityIdIndex,
metadata::{ShiftKeyDown, Sprinting},
update_bounding_box,
};
use azalea_physics::PhysicsSet;
use azalea_protocol::packets::game::s_interact::{self, ServerboundInteract};
use azalea_world::MinecraftEntityId;
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use tracing::warn;
use super::packet::game::SendPacketEvent;
use crate::{
@ -35,7 +36,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(),
);
@ -44,65 +46,94 @@ impl Plugin for AttackPlugin {
impl Client {
/// Attack the entity with the given id.
pub fn attack(&self, entity_id: MinecraftEntityId) {
pub fn attack(&self, entity: Entity) {
self.ecs.lock().send_event(AttackEvent {
entity: self.entity,
target: entity_id,
target: entity,
});
}
/// Whether the player has an attack cooldown.
///
/// Also see [`Client::attack_cooldown_remaining_ticks`].
pub fn has_attack_cooldown(&self) -> bool {
let Some(AttackStrengthScale(ticks_since_last_attack)) =
self.get_component::<AttackStrengthScale>()
else {
// they don't even have an AttackStrengthScale so they probably can't attack
// lmao, just return false
let Some(attack_strength_scale) = self.get_component::<AttackStrengthScale>() else {
// they don't even have an AttackStrengthScale so they probably can't even
// attack? whatever, just return false
return false;
};
ticks_since_last_attack < 1.0
*attack_strength_scale < 1.0
}
/// Returns the number of ticks until we can attack at full strength again.
///
/// Also see [`Client::has_attack_cooldown`].
pub fn attack_cooldown_remaining_ticks(&self) -> usize {
let mut ecs = self.ecs.lock();
let Ok((attributes, ticks_since_last_attack)) = ecs
.query::<(&Attributes, &TicksSinceLastAttack)>()
.get(&ecs, self.entity)
else {
return 0;
};
let attack_strength_delay = get_attack_strength_delay(attributes);
let remaining_ticks = attack_strength_delay - **ticks_since_last_attack as f32;
remaining_ticks.max(0.).ceil() as usize
}
}
/// A component that indicates that this client will be attacking the given
/// entity next tick.
#[derive(Component, Clone, Debug)]
struct AttackQueued {
pub target: MinecraftEntityId,
pub struct AttackQueued {
pub target: Entity,
}
fn handle_attack_queued(
#[allow(clippy::type_complexity)]
pub fn handle_attack_queued(
mut commands: Commands,
mut query: Query<(
Entity,
&AttackQueued,
&LocalGameMode,
&mut TicksSinceLastAttack,
&mut Physics,
&mut Sprinting,
&AttackQueued,
&LocalGameMode,
&ShiftKeyDown,
&EntityIdIndex,
)>,
) {
for (
entity,
attack_queued,
game_mode,
client_entity,
mut ticks_since_last_attack,
mut physics,
mut sprinting,
attack_queued,
game_mode,
sneaking,
entity_id_index,
) in &mut query
{
commands.entity(entity).remove::<AttackQueued>();
let target_entity = attack_queued.target;
let Some(target_entity_id) = entity_id_index.get_by_ecs_entity(target_entity) else {
warn!("tried to attack entity {target_entity} which isn't in our EntityIdIndex");
continue;
};
commands.entity(client_entity).remove::<AttackQueued>();
commands.trigger(SendPacketEvent::new(
entity,
client_entity,
ServerboundInteract {
entity_id: attack_queued.target,
entity_id: target_entity_id,
action: s_interact::ActionType::Attack,
using_secondary_action: **sneaking,
},
));
commands.trigger(SwingArmEvent { entity });
commands.trigger(SwingArmEvent {
entity: client_entity,
});
// we can't attack if we're in spectator mode but it still sends the attack
// packet
@ -123,7 +154,8 @@ fn handle_attack_queued(
pub struct AttackEvent {
/// Our client entity that will send the packets to attack.
pub entity: Entity,
pub target: MinecraftEntityId,
/// The entity that will be attacked.
pub target: Entity,
}
pub fn handle_attack_event(mut events: EventReader<AttackEvent>, mut commands: Commands) {
for event in events.read() {

View file

@ -0,0 +1,142 @@
//! 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> {
let delay = if let Ok(c) = auto_reconnect_delay_query.get(entity) {
Some(c.delay)
} else {
auto_reconnect_delay_res.as_ref().map(|r| r.delay)
};
if delay == Some(Duration::MAX) {
// if the duration is set to max, treat that as autoreconnect being disabled
return None;
}
delay
}
pub fn rejoin_after_delay(
mut commands: Commands,
mut join_events: EventWriter<StartJoinServerEvent>,
query: Query<(
Entity,
&InternalReconnectAfter,
&Account,
&ConnectOpts,
Option<&LocalPlayerEvents>,
)>,
) {
for (entity, reconnect_after, account, connect_opts, local_player_events) in query.iter() {
if Instant::now() >= reconnect_after.instant {
// don't keep trying to reconnect
commands.entity(entity).remove::<InternalReconnectAfter>();
// our Entity will be reused since the account has the same uuid
join_events.write(StartJoinServerEvent {
account: account.clone(),
connect_opts: connect_opts.clone(),
// not actually necessary since we're reusing the same entity and LocalPlayerEvents
// isn't removed, but this is more readable and just in case it's changed in the
// future
event_sender: local_player_events.map(|e| e.0.clone()),
start_join_callback_tx: None,
});
}
}
}
/// A resource *and* component that indicates how long to wait before
/// reconnecting when we're kicked.
///
/// Initially, it's a resource in the ECS set to 5 seconds. You can modify
/// the resource to update the global reconnect delay, or insert it as a
/// component to set the individual delay for a single client.
///
/// You can also remove this resource from the ECS to disable the default
/// auto-reconnecting behavior. Inserting the resource/component again will not
/// make clients that were already disconnected automatically reconnect.
#[derive(Resource, Component, Debug, Clone)]
pub struct AutoReconnectDelay {
pub delay: Duration,
}
impl AutoReconnectDelay {
pub fn new(delay: Duration) -> Self {
Self { delay }
}
}
/// This is inserted when we're disconnected and indicates when we'll reconnect.
///
/// This is set based on [`AutoReconnectDelay`].
#[derive(Component, Debug, Clone)]
pub struct InternalReconnectAfter {
pub instant: Instant,
}

View file

@ -0,0 +1,49 @@
use azalea_block::BlockState;
use azalea_core::position::BlockPos;
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use crate::{
chunks::handle_receive_chunk_event, interact::BlockStatePredictionHandler,
local_player::InstanceHolder,
};
pub struct BlockUpdatePlugin;
impl Plugin for BlockUpdatePlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
// has to be after ReceiveChunkEvent is handled so if we get chunk+blockupdate in one
// Update then the block update actually gets applied
handle_block_update_event.after(handle_receive_chunk_event),
);
}
}
/// A component that holds the list of block updates that need to be handled.
///
/// This is updated by `read_packets` (in `PreUpdate`) and handled/cleared by
/// [`handle_block_update_event`] (`Update`).
///
/// This is a component instead of an ECS event for performance reasons.
#[derive(Component, Debug, Clone, Default)]
pub struct QueuedServerBlockUpdates {
pub list: Vec<(BlockPos, BlockState)>,
}
pub fn handle_block_update_event(
mut query: Query<(
&mut QueuedServerBlockUpdates,
&InstanceHolder,
&mut BlockStatePredictionHandler,
)>,
) {
for (mut queued, instance_holder, mut prediction_handler) in query.iter_mut() {
let world = instance_holder.instance.read();
for (pos, block_state) in queued.list.drain(..) {
if !prediction_handler.update_known_server_state(pos, block_state) {
world.chunks.set_block_state(pos, block_state);
}
}
}
}

View file

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

View file

@ -10,12 +10,7 @@ use azalea_protocol::packets::game::{
c_system_chat::ClientboundSystemChat,
};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::{
entity::Entity,
event::{EventReader, EventWriter},
prelude::Event,
schedule::IntoSystemConfigs,
};
use bevy_ecs::prelude::*;
use handler::{SendChatKindEvent, handle_send_chat_kind_event};
use uuid::Uuid;
@ -129,9 +124,9 @@ impl ChatPacket {
}))
}
/// Whether this message was sent with /msg (or aliases). It works by
/// checking the translation key, so it won't work on servers that use their
/// own whisper system.
/// Whether this message is an incoming whisper message (i.e. someone else
/// dm'd the bot with /msg). It works by checking the translation key, so it
/// won't work on servers that use their own whisper system.
pub fn is_whisper(&self) -> bool {
match self.message() {
FormattedText::Text(_) => false,
@ -152,7 +147,6 @@ impl Client {
content: message.to_string(),
kind: ChatKind::Message,
});
let _ = self.run_schedule_sender.try_send(());
}
/// Send a command packet to the server. The `command` argument should not
@ -166,7 +160,6 @@ impl Client {
content: command.to_string(),
kind: ChatKind::Command,
});
let _ = self.run_schedule_sender.try_send(());
}
/// Send a message in chat.
@ -183,7 +176,6 @@ impl Client {
entity: self.entity,
content: content.to_string(),
});
let _ = self.run_schedule_sender.try_send(());
}
}
@ -207,13 +199,13 @@ pub fn handle_send_chat_event(
) {
for event in events.read() {
if event.content.starts_with('/') {
send_chat_kind_events.send(SendChatKindEvent {
send_chat_kind_events.write(SendChatKindEvent {
entity: event.entity,
content: event.content[1..].to_string(),
kind: ChatKind::Command,
});
} else {
send_chat_kind_events.send(SendChatKindEvent {
send_chat_kind_events.write(SendChatKindEvent {
entity: event.entity,
content: event.content.clone(),
kind: ChatKind::Message,

View file

@ -0,0 +1,184 @@
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
&& 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 && let Some(access_token) = &account.access_token {
let task_pool = IoTaskPool::get();
let access_token = access_token.lock().clone();
debug!("Started task to fetch certs");
let task = task_pool.spawn(async_compat::Compat::new(async move {
azalea_auth::certs::fetch_certificates(&access_token).await
}));
commands
.entity(entity)
.insert(RequestCertsTask(task))
.remove::<OnlyRefreshCertsAfter>();
}
}
}
/// A component that's present on players that should send their chat signing
/// certificates as soon as possible.
///
/// This is removed when the certificates get sent.
#[derive(Component)]
pub struct QueuedCertsToSend {
pub certs: Certificates,
}
pub fn handle_queued_certs_to_send(
mut commands: Commands,
query: Query<(Entity, &QueuedCertsToSend), With<IsAuthenticated>>,
) {
for (entity, queued_certs) in &query {
let certs = &queued_certs.certs;
let session_id = Uuid::new_v4();
let chat_session = RemoteChatSessionData {
session_id,
profile_public_key: ProfilePublicKeyData {
expires_at: certs.expires_at.timestamp_millis() as u64,
key: certs.public_key_der.clone(),
key_signature: certs.signature_v2.clone(),
},
};
debug!("Sending chat signing certs to server");
commands.trigger(SendPacketEvent::new(
entity,
ServerboundChatSessionUpdate { chat_session },
));
commands
.entity(entity)
.remove::<QueuedCertsToSend>()
.insert(ChatSigningSession {
session_id,
messages_sent: 0,
});
}
}

View file

@ -17,8 +17,8 @@ use bevy_ecs::prelude::*;
use tracing::{error, trace};
use crate::{
InstanceHolder, interact::handle_block_interact_event, inventory::InventorySet,
packet::game::SendPacketEvent, respawn::perform_respawn,
inventory::InventorySet, local_player::InstanceHolder, packet::game::SendPacketEvent,
respawn::perform_respawn,
};
pub struct ChunksPlugin;
@ -28,12 +28,11 @@ impl Plugin for ChunksPlugin {
Update,
(
handle_chunk_batch_start_event,
handle_receive_chunk_events,
handle_receive_chunk_event,
handle_chunk_batch_finished_event,
)
.chain()
.before(InventorySet)
.before(handle_block_interact_event)
.before(perform_respawn),
)
.add_event::<ReceiveChunkEvent>()
@ -65,9 +64,9 @@ pub struct ChunkBatchFinishedEvent {
pub batch_size: u32,
}
pub fn handle_receive_chunk_events(
pub fn handle_receive_chunk_event(
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);
@ -84,14 +83,12 @@ pub fn handle_receive_chunk_events(
let shared_chunk = instance.chunks.get(&pos);
let this_client_has_chunk = partial_instance.chunks.limited_get(&pos).is_some();
if !this_client_has_chunk {
if let Some(shared_chunk) = shared_chunk {
trace!("Skipping parsing chunk {pos:?} because we already know about it");
partial_instance
.chunks
.limited_set(&pos, Some(shared_chunk));
continue;
}
if !this_client_has_chunk && let Some(shared_chunk) = shared_chunk {
trace!("Skipping parsing chunk {pos:?} because we already know about it");
partial_instance
.chunks
.limited_set(&pos, Some(shared_chunk));
continue;
}
let heightmaps = &event.packet.chunk_data.heightmaps;

View file

@ -0,0 +1,375 @@
use std::{fmt::Debug, io::Cursor, mem, sync::Arc};
use azalea_crypto::Aes128CfbEnc;
use azalea_protocol::{
connect::{RawReadConnection, RawWriteConnection},
packets::{
ConnectionProtocol, Packet, ProtocolPacket, config::ClientboundConfigPacket,
game::ClientboundGamePacket, login::ClientboundLoginPacket,
},
read::{ReadPacketError, deserialize_packet},
write::serialize_packet,
};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_tasks::{IoTaskPool, futures_lite::future};
use thiserror::Error;
use tokio::{
io::AsyncWriteExt,
net::tcp::OwnedWriteHalf,
sync::mpsc::{self},
};
use tracing::{debug, error, info, trace};
use super::packet::{
config::ReceiveConfigPacketEvent, game::ReceiveGamePacketEvent, login::ReceiveLoginPacketEvent,
};
use crate::packet::{config, game, login};
pub struct ConnectionPlugin;
impl Plugin for ConnectionPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreUpdate, (read_packets, poll_all_writer_tasks).chain());
}
}
pub fn read_packets(ecs: &mut World) {
let mut entity_and_conn_query = ecs.query::<(Entity, &mut RawConnection)>();
let mut conn_query = ecs.query::<&mut RawConnection>();
let mut entities_handling_packets = Vec::new();
let mut entities_with_injected_packets = Vec::new();
for (entity, mut raw_conn) in entity_and_conn_query.iter_mut(ecs) {
if !raw_conn.injected_clientbound_packets.is_empty() {
entities_with_injected_packets.push((
entity,
mem::take(&mut raw_conn.injected_clientbound_packets),
));
}
if raw_conn.network.is_none() {
// no network connection, don't bother with the normal packet handling
continue;
}
entities_handling_packets.push(entity);
}
let mut queued_packet_events = QueuedPacketEvents::default();
// handle injected packets, see the comment on
// RawConnection::injected_clientbound_packets for more info
for (entity, raw_packets) in entities_with_injected_packets {
for raw_packet in raw_packets {
let conn = conn_query.get(ecs, entity).unwrap();
let state = conn.state;
trace!("Received injected packet with bytes: {raw_packet:?}");
if let Err(e) =
handle_raw_packet(ecs, &raw_packet, entity, state, &mut queued_packet_events)
{
error!("Error reading injected packet: {e}");
}
}
}
for entity in entities_handling_packets {
loop {
let mut conn = conn_query.get_mut(ecs, entity).unwrap();
let net_conn = conn.net_conn().unwrap();
let read_res = net_conn.reader.try_read();
let state = conn.state;
match read_res {
Ok(Some(raw_packet)) => {
let raw_packet = Arc::<[u8]>::from(raw_packet);
if let Err(e) = handle_raw_packet(
ecs,
&raw_packet,
entity,
state,
&mut queued_packet_events,
) {
error!("Error reading packet: {e}");
}
}
Ok(None) => {
// no packets available
break;
}
Err(err) => {
log_for_error(&err);
if matches!(
&*err,
ReadPacketError::IoError { .. } | ReadPacketError::ConnectionClosed
) {
info!("Server closed connection");
// ungraceful disconnect :(
conn.network = None;
// setting this will make us send a DisconnectEvent
conn.is_alive = false;
}
break;
}
}
}
}
queued_packet_events.send_events(ecs);
}
fn poll_all_writer_tasks(mut conn_query: Query<&mut RawConnection>) {
for mut conn in conn_query.iter_mut() {
if let Some(net_conn) = &mut conn.network {
// this needs to be done at some point every update to make sure packets are
// actually sent to the network
if net_conn.poll_writer().is_some() {
// means the writer task ended
conn.network = None;
conn.is_alive = false;
}
}
}
}
#[derive(Default)]
pub struct QueuedPacketEvents {
login: Vec<ReceiveLoginPacketEvent>,
config: Vec<ReceiveConfigPacketEvent>,
game: Vec<ReceiveGamePacketEvent>,
}
impl QueuedPacketEvents {
fn send_events(&mut self, ecs: &mut World) {
ecs.send_event_batch(self.login.drain(..));
ecs.send_event_batch(self.config.drain(..));
ecs.send_event_batch(self.game.drain(..));
}
}
fn log_for_error(error: &ReadPacketError) {
if !matches!(*error, ReadPacketError::ConnectionClosed) {
error!("Error reading packet from Client: {error:?}");
}
}
/// The client's connection to the server.
#[derive(Component)]
pub struct RawConnection {
/// The network connection to the server.
///
/// This isn't guaranteed to be present, for example during the main packet
/// handlers or at all times during tests.
///
/// You shouldn't rely on this. Instead, use the events for sending packets
/// like [`SendPacketEvent`](crate::packet::game::SendPacketEvent) /
/// [`SendConfigPacketEvent`](crate::packet::config::SendConfigPacketEvent)
/// / [`SendLoginPacketEvent`](crate::packet::login::SendLoginPacketEvent).
///
/// To check if we haven't disconnected from the server, use
/// [`Self::is_alive`].
pub(crate) network: Option<NetworkConnection>,
pub state: ConnectionProtocol,
pub(crate) is_alive: bool,
/// This exists for internal testing purposes and probably shouldn't be used
/// for normal bots. It's basically a way to make our client think it
/// received a packet from the server without needing to interact with the
/// network.
pub injected_clientbound_packets: Vec<Box<[u8]>>,
}
impl RawConnection {
pub fn new(
reader: RawReadConnection,
writer: RawWriteConnection,
state: ConnectionProtocol,
) -> Self {
let task_pool = IoTaskPool::get();
let (network_packet_writer_tx, network_packet_writer_rx) =
mpsc::unbounded_channel::<Box<[u8]>>();
let writer_task =
task_pool.spawn(write_task(network_packet_writer_rx, writer.write_stream));
let mut conn = Self::new_networkless(state);
conn.network = Some(NetworkConnection {
reader,
enc_cipher: writer.enc_cipher,
network_packet_writer_tx,
writer_task,
});
conn
}
pub fn new_networkless(state: ConnectionProtocol) -> Self {
Self {
network: None,
state,
is_alive: true,
injected_clientbound_packets: Vec::new(),
}
}
pub fn is_alive(&self) -> bool {
self.is_alive
}
/// Write a packet to the server without emitting any events.
///
/// This is called by the handlers for [`SendPacketEvent`],
/// [`SendConfigPacketEvent`], and [`SendLoginPacketEvent`].
///
/// [`SendPacketEvent`]: crate::packet::game::SendPacketEvent
/// [`SendConfigPacketEvent`]: crate::packet::config::SendConfigPacketEvent
/// [`SendLoginPacketEvent`]: crate::packet::login::SendLoginPacketEvent
pub fn write<P: ProtocolPacket + Debug>(
&mut self,
packet: impl Packet<P>,
) -> Result<(), WritePacketError> {
if let Some(network) = &mut self.network {
network.write(packet)?;
} else {
debug!(
"tried to write packet to the network but there is no NetworkConnection. if you're trying to send a packet from the handler function, use self.write instead"
);
}
Ok(())
}
pub fn net_conn(&mut self) -> Option<&mut NetworkConnection> {
self.network.as_mut()
}
}
pub fn handle_raw_packet(
ecs: &mut World,
raw_packet: &[u8],
entity: Entity,
state: ConnectionProtocol,
queued_packet_events: &mut QueuedPacketEvents,
) -> Result<(), Box<ReadPacketError>> {
let stream = &mut Cursor::new(raw_packet);
match state {
ConnectionProtocol::Handshake => {
unreachable!()
}
ConnectionProtocol::Game => {
let packet = Arc::new(deserialize_packet::<ClientboundGamePacket>(stream)?);
trace!("Packet: {packet:?}");
game::process_packet(ecs, entity, packet.as_ref());
queued_packet_events
.game
.push(ReceiveGamePacketEvent { entity, packet });
}
ConnectionProtocol::Status => {
unreachable!()
}
ConnectionProtocol::Login => {
let packet = Arc::new(deserialize_packet::<ClientboundLoginPacket>(stream)?);
trace!("Packet: {packet:?}");
login::process_packet(ecs, entity, &packet);
queued_packet_events
.login
.push(ReceiveLoginPacketEvent { entity, packet });
}
ConnectionProtocol::Configuration => {
let packet = Arc::new(deserialize_packet::<ClientboundConfigPacket>(stream)?);
trace!("Packet: {packet:?}");
config::process_packet(ecs, entity, &packet);
queued_packet_events
.config
.push(ReceiveConfigPacketEvent { entity, packet });
}
};
Ok(())
}
pub struct NetworkConnection {
reader: RawReadConnection,
// compression threshold is in the RawReadConnection
pub enc_cipher: Option<Aes128CfbEnc>,
pub writer_task: bevy_tasks::Task<()>,
/// A queue of raw TCP packets to send. These will not be modified further,
/// they should already be serialized and encrypted and everything before
/// being added here.
network_packet_writer_tx: mpsc::UnboundedSender<Box<[u8]>>,
}
impl NetworkConnection {
pub fn write<P: ProtocolPacket + Debug>(
&mut self,
packet: impl Packet<P>,
) -> Result<(), WritePacketError> {
let packet = packet.into_variant();
let raw_packet = serialize_packet(&packet)?;
self.write_raw(&raw_packet)?;
Ok(())
}
pub fn write_raw(&mut self, raw_packet: &[u8]) -> Result<(), WritePacketError> {
let network_packet = azalea_protocol::write::encode_to_network_packet(
raw_packet,
self.reader.compression_threshold,
&mut self.enc_cipher,
);
self.network_packet_writer_tx
.send(network_packet.into_boxed_slice())?;
Ok(())
}
/// Makes sure packets get sent and returns Some(()) if the connection has
/// closed.
pub fn poll_writer(&mut self) -> Option<()> {
let poll_once_res = future::poll_once(&mut self.writer_task);
future::block_on(poll_once_res)
}
pub fn set_compression_threshold(&mut self, threshold: Option<u32>) {
trace!("Set compression threshold to {threshold:?}");
self.reader.compression_threshold = threshold;
}
/// Set the encryption key that is used to encrypt and decrypt packets. It's
/// the same for both reading and writing.
pub fn set_encryption_key(&mut self, key: [u8; 16]) {
trace!("Enabled protocol encryption");
let (enc_cipher, dec_cipher) = azalea_crypto::create_cipher(&key);
self.reader.dec_cipher = Some(dec_cipher);
self.enc_cipher = Some(enc_cipher);
}
}
async fn write_task(
mut network_packet_writer_rx: mpsc::UnboundedReceiver<Box<[u8]>>,
mut write_half: OwnedWriteHalf,
) {
while let Some(network_packet) = network_packet_writer_rx.recv().await {
if let Err(e) = write_half.write_all(&network_packet).await {
debug!("Error writing packet to server: {e}");
break;
};
}
trace!("write task is done");
}
#[derive(Error, Debug)]
pub enum WritePacketError {
#[error("Wrong protocol state: expected {expected:?}, got {got:?}")]
WrongState {
expected: ConnectionProtocol,
got: ConnectionProtocol,
},
#[error(transparent)]
Encoding(#[from] azalea_protocol::write::PacketEncodeError),
#[error(transparent)]
SendError {
#[from]
#[backtrace]
source: mpsc::error::SendError<Box<[u8]>>,
},
}

View file

@ -2,22 +2,16 @@
use azalea_chat::FormattedText;
use azalea_entity::{EntityBundle, InLoadedChunk, LocalEntity, metadata::PlayerMetadataBundle};
use azalea_world::MinecraftEntityId;
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 super::login::IsAuthenticated;
use crate::{
InstanceHolder, client::JoinedClientBundle, events::LocalPlayerEvents,
raw_connection::RawConnection,
chat_signing, client::JoinedClientBundle, connection::RawConnection, loading::HasClientLoaded,
local_player::InstanceHolder, tick_counter::TicksConnected,
};
pub struct DisconnectPlugin;
@ -27,21 +21,61 @@ 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 minecraft_entity_id: MinecraftEntityId,
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,
// send ServerboundPlayerLoaded next time we join.
pub has_client_loaded: HasClientLoaded,
// TickCounter is reset on reconnect
pub ticks_alive: TicksConnected,
}
/// A system that removes the several components from our clients when they get
/// a [`DisconnectEvent`].
pub fn remove_components_from_disconnected_players(
@ -49,19 +83,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 +127,7 @@ fn disconnect_on_connection_dead(
) {
for (entity, &is_connection_alive) in &query {
if !*is_connection_alive {
disconnect_events.send(DisconnectEvent {
disconnect_events.write(DisconnectEvent {
entity,
reason: None,
});

View file

@ -1,35 +1,26 @@
//! 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;
use azalea_chat::FormattedText;
use azalea_core::tick::GameTick;
use azalea_core::{position::ChunkPos, tick::GameTick};
use azalea_entity::{Dead, InLoadedChunk};
use azalea_protocol::packets::game::{
ClientboundGamePacket, c_player_combat_kill::ClientboundPlayerCombatKill,
};
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;
use crate::{
PlayerInfo,
chat::{ChatPacket, ChatReceivedEvent},
chunks::ReceiveChunkEvent,
disconnect::DisconnectEvent,
packet::game::{
AddPlayerEvent, DeathEvent, KeepAliveEvent, ReceivePacketEvent, RemovePlayerEvent,
UpdatePlayerEvent,
AddPlayerEvent, DeathEvent, KeepAliveEvent, RemovePlayerEvent, UpdatePlayerEvent,
},
player::PlayerInfo,
};
// (for contributors):
@ -94,6 +85,7 @@ pub enum Event {
Chat(ChatPacket),
/// Happens 20 times per second, but only when the world is loaded.
Tick,
#[cfg(feature = "packet-event")]
/// We received a packet from the server.
///
/// ```
@ -111,7 +103,7 @@ pub enum Event {
/// # }
/// # }
/// ```
Packet(Arc<ClientboundGamePacket>),
Packet(Arc<azalea_protocol::packets::game::ClientboundGamePacket>),
/// A player joined the game (or more specifically, was added to the tab
/// list).
AddPlayer(PlayerInfo),
@ -127,6 +119,7 @@ pub enum Event {
KeepAlive(u64),
/// The client disconnected from the server.
Disconnect(Option<FormattedText>),
ReceiveChunk(ChunkPos),
}
/// A component that contains an event sender for events that are only
@ -146,6 +139,7 @@ impl Plugin for EventsPlugin {
chat_listener,
login_listener,
spawn_listener,
#[cfg(feature = "packet-event")]
packet_listener,
add_player_listener,
update_player_listener,
@ -153,11 +147,12 @@ impl Plugin for EventsPlugin {
keepalive_listener,
death_listener,
disconnect_listener,
receive_chunk_listener,
),
)
.add_systems(
PreUpdate,
init_listener.before(crate::packet::game::process_packet_events),
init_listener.before(super::connection::read_packets),
)
.add_systems(GameTick, tick_listener);
}
@ -215,9 +210,10 @@ pub fn tick_listener(query: Query<&LocalPlayerEvents, With<InstanceName>>) {
}
}
#[cfg(feature = "packet-event")]
pub fn packet_listener(
query: Query<&LocalPlayerEvents>,
mut events: EventReader<ReceivePacketEvent>,
mut events: EventReader<super::packet::game::ReceiveGamePacketEvent>,
) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
@ -301,3 +297,17 @@ pub fn disconnect_listener(
}
}
}
pub fn receive_chunk_listener(
query: Query<&LocalPlayerEvents>,
mut events: EventReader<ReceiveChunkEvent>,
) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
let _ = local_player_events.send(Event::ReceiveChunk(ChunkPos::new(
event.packet.x,
event.packet.z,
)));
}
}
}

View file

@ -1,370 +0,0 @@
use std::ops::AddAssign;
use azalea_block::BlockState;
use azalea_core::{
block_hit_result::BlockHitResult,
direction::Direction,
game_type::GameMode,
position::{BlockPos, Vec3},
};
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_protocol::packets::game::{
s_interact::InteractionHand,
s_swing::ServerboundSwing,
s_use_item_on::{BlockHit, 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 derive_more::{Deref, DerefMut};
use tracing::warn;
use crate::{
Client,
attack::handle_attack_event,
inventory::{Inventory, InventorySet},
local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
movement::MoveEventsSet,
packet::game::SendPacketEvent,
respawn::perform_respawn,
};
/// A plugin that allows clients to interact with blocks in the world.
pub struct InteractPlugin;
impl Plugin for InteractPlugin {
fn build(&self, app: &mut App) {
app.add_event::<BlockInteractEvent>()
.add_event::<SwingArmEvent>()
.add_systems(
Update,
(
(
update_hit_result_component.after(clamp_look_direction),
handle_block_interact_event,
handle_swing_arm_event,
)
.after(InventorySet)
.after(perform_respawn)
.after(handle_attack_event)
.chain(),
update_modifiers_for_held_item
.after(InventorySet)
.after(MoveEventsSet),
),
)
.add_observer(handle_swing_arm_trigger);
}
}
impl Client {
/// 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 {
entity: self.entity,
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,
}
/// A component that contains the number of changes this client has made to
/// blocks.
#[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;
}
}
/// A component that contains the block that the player is currently looking at.
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct HitResultComponent(BlockHitResult);
pub fn handle_block_interact_event(
mut events: EventReader<BlockInteractEvent>,
mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>,
mut commands: Commands,
) {
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;
};
// TODO: check to make sure we're within the world border
*sequence_number += 1;
// minecraft also does the interaction client-side (so it looks like clicking a
// button is instant) but we don't really need that
// the block_hit data will depend on whether we're looking at the block and
// whether we can reach it
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,
}
} 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,
},
));
}
}
#[allow(clippy::type_complexity)]
pub fn update_hit_result_component(
mut commands: Commands,
mut query: Query<(
Entity,
Option<&mut HitResultComponent>,
&LocalGameMode,
&Position,
&EyeHeight,
&LookDirection,
&InstanceName,
)>,
instance_container: Res<InstanceContainer>,
) {
for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in
&mut query
{
let pick_range = if game_mode.current == GameMode::Creative {
6.
} else {
4.5
};
let eye_position = Vec3 {
x: position.x,
y: position.y + **eye_height as f64,
z: position.z,
};
let Some(instance_lock) = instance_container.get(world_name) else {
continue;
};
let instance = instance_lock.read();
let hit_result = pick(look_direction, &eye_position, &instance.chunks, pick_range);
if let Some(mut hit_result_ref) = hit_result_ref {
**hit_result_ref = hit_result;
} else {
commands
.entity(entity)
.insert(HitResultComponent(hit_result));
}
}
}
/// 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(
look_direction: &LookDirection,
eye_position: &Vec3,
chunks: &azalea_world::ChunkStorage,
pick_range: f64,
) -> BlockHitResult {
let view_vector = view_vector(look_direction);
let end_position = eye_position + &(view_vector * pick_range);
azalea_physics::clip::clip(
chunks,
ClipContext {
from: *eye_position,
to: end_position,
block_shape_type: BlockShapeType::Outline,
fluid_pick_type: FluidPickType::None,
},
)
}
/// Whether we can't interact with the block, based on your gamemode. If
/// this is false, then we can interact with the block.
///
/// Passing the inventory, block position, and instance is necessary for the
/// adventure mode check.
pub fn check_is_interaction_restricted(
instance: &Instance,
block_pos: &BlockPos,
game_mode: &GameMode,
inventory: &Inventory,
) -> bool {
match game_mode {
GameMode::Adventure => {
// vanilla checks for abilities.mayBuild here but servers have no
// way of modifying that
let held_item = inventory.held_item();
match &held_item {
ItemStack::Present(item) => {
let block = instance.chunks.get_block_state(block_pos);
let Some(block) = block else {
// block isn't loaded so just say that it is restricted
return true;
};
check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
}
_ => true,
}
}
GameMode::Spectator => true,
_ => false,
}
}
/// Check if the item has the `CanDestroy` tag for the block.
pub fn check_block_can_be_broken_by_item_in_adventure_mode(
item: &ItemStackData,
_block: &BlockState,
) -> bool {
// minecraft caches the last checked block but that's kind of an unnecessary
// optimization and makes the code too complicated
if !item.components.has::<components::CanBreak>() {
// no CanDestroy tag
return false;
};
false
// for block_predicate in can_destroy {
// // TODO
// // defined in BlockPredicateArgument.java
// }
// true
}
pub fn can_use_game_master_blocks(
abilities: &PlayerAbilities,
permission_level: &PermissionLevel,
) -> bool {
abilities.instant_break && **permission_level >= 2
}
/// Swing your arm. This is purely a visual effect and won't interact with
/// anything in the world.
#[derive(Event, Clone, Debug)]
pub struct SwingArmEvent {
pub entity: Entity,
}
pub fn handle_swing_arm_trigger(trigger: Trigger<SwingArmEvent>, mut commands: Commands) {
commands.trigger(SendPacketEvent::new(
trigger.event().entity,
ServerboundSwing {
hand: InteractionHand::MainHand,
},
));
}
pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut commands: Commands) {
for event in events.read() {
commands.trigger(event.clone());
}
}
#[allow(clippy::type_complexity)]
fn update_modifiers_for_held_item(
mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
) {
for (mut attributes, inventory) in &mut query {
let held_item = inventory.held_item();
use azalea_registry::Item;
let added_attack_speed = match held_item.kind() {
Item::WoodenSword => -2.4,
Item::WoodenShovel => -3.0,
Item::WoodenPickaxe => -2.8,
Item::WoodenAxe => -3.2,
Item::WoodenHoe => -3.0,
Item::StoneSword => -2.4,
Item::StoneShovel => -3.0,
Item::StonePickaxe => -2.8,
Item::StoneAxe => -3.2,
Item::StoneHoe => -2.0,
Item::GoldenSword => -2.4,
Item::GoldenShovel => -3.0,
Item::GoldenPickaxe => -2.8,
Item::GoldenAxe => -3.0,
Item::GoldenHoe => -3.0,
Item::IronSword => -2.4,
Item::IronShovel => -3.0,
Item::IronPickaxe => -2.8,
Item::IronAxe => -3.1,
Item::IronHoe => -1.0,
Item::DiamondSword => -2.4,
Item::DiamondShovel => -3.0,
Item::DiamondPickaxe => -2.8,
Item::DiamondAxe => -3.0,
Item::DiamondHoe => 0.0,
Item::NetheriteSword => -2.4,
Item::NetheriteShovel => -3.0,
Item::NetheritePickaxe => -2.8,
Item::NetheriteAxe => -3.0,
Item::NetheriteHoe => 0.0,
Item::Trident => -2.9,
_ => 0.,
};
attributes
.attack_speed
.insert(azalea_entity::attributes::base_attack_speed_modifier(
added_attack_speed,
));
}
}

View file

@ -0,0 +1,505 @@
pub mod pick;
use std::collections::HashMap;
use azalea_block::BlockState;
use azalea_core::{
direction::Direction,
game_type::GameMode,
hit_result::{BlockHitResult, HitResult},
position::{BlockPos, Vec3},
tick::GameTick,
};
use azalea_entity::{
Attributes, LocalEntity, LookDirection,
attributes::{
creative_block_interaction_range_modifier, creative_entity_interaction_range_modifier,
},
clamp_look_direction,
};
use azalea_inventory::{ItemStack, ItemStackData, components};
use azalea_physics::PhysicsSet;
use azalea_protocol::packets::game::{
ServerboundInteract, ServerboundUseItem,
s_interact::{self, InteractionHand},
s_swing::ServerboundSwing,
s_use_item_on::ServerboundUseItemOn,
};
use azalea_world::{Instance, MinecraftEntityId};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use tracing::warn;
use super::mining::Mining;
use crate::{
Client,
attack::handle_attack_event,
interact::pick::{HitResultComponent, update_hit_result_component},
inventory::{Inventory, InventorySet},
local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
movement::MoveEventsSet,
packet::game::SendPacketEvent,
respawn::perform_respawn,
};
/// A plugin that allows clients to interact with blocks in the world.
pub struct InteractPlugin;
impl Plugin for InteractPlugin {
fn build(&self, app: &mut App) {
app.add_event::<StartUseItemEvent>()
.add_event::<SwingArmEvent>()
.add_systems(
Update,
(
(
update_attributes_for_held_item,
update_attributes_for_gamemode,
)
.in_set(UpdateAttributesSet)
.chain(),
handle_start_use_item_event,
update_hit_result_component.after(clamp_look_direction),
handle_swing_arm_event,
)
.after(InventorySet)
.after(MoveEventsSet)
.after(perform_respawn)
.after(handle_attack_event)
.chain(),
)
.add_systems(GameTick, handle_start_use_item_queued.before(PhysicsSet))
.add_observer(handle_swing_arm_trigger);
}
}
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub struct UpdateAttributesSet;
impl Client {
/// 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(StartUseItemEvent {
entity: self.entity,
hand: InteractionHand::MainHand,
force_block: Some(position),
});
}
/// 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 information about our local block state
/// predictions.
#[derive(Component, Clone, Debug, Default)]
pub struct BlockStatePredictionHandler {
/// The total number of changes that this client has made to blocks.
seq: u32,
server_state: HashMap<BlockPos, ServerVerifiedState>,
}
#[derive(Clone, Debug)]
struct ServerVerifiedState {
seq: u32,
block_state: BlockState,
/// Used for teleporting the player back if we're colliding with the block
/// that got placed back.
#[allow(unused)]
player_pos: Vec3,
}
impl BlockStatePredictionHandler {
/// Get the next sequence number that we're going to use and increment the
/// value.
pub fn start_predicting(&mut self) -> u32 {
self.seq += 1;
self.seq
}
/// Should be called right before the client updates a block with its
/// prediction.
///
/// This is used to make sure that we can rollback to this state if the
/// server acknowledges the sequence number (with
/// [`ClientboundBlockChangedAck`]) without having sent a block update.
///
/// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
pub fn retain_known_server_state(
&mut self,
pos: BlockPos,
old_state: BlockState,
player_pos: Vec3,
) {
self.server_state
.entry(pos)
.and_modify(|s| s.seq = self.seq)
.or_insert(ServerVerifiedState {
seq: self.seq,
block_state: old_state,
player_pos,
});
}
/// Save this update as the correct server state so when the server sends a
/// [`ClientboundBlockChangedAck`] we don't roll back this new update.
///
/// This should be used when we receive a block update from the server.
///
/// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
pub fn update_known_server_state(&mut self, pos: BlockPos, state: BlockState) -> bool {
if let Some(s) = self.server_state.get_mut(&pos) {
s.block_state = state;
true
} else {
false
}
}
pub fn end_prediction_up_to(&mut self, seq: u32, world: &Instance) {
let mut to_remove = Vec::new();
for (pos, state) in &self.server_state {
if state.seq > seq {
continue;
}
to_remove.push(*pos);
// syncBlockState
let client_block_state = world.get_block_state(*pos).unwrap_or_default();
let server_block_state = state.block_state;
if client_block_state == server_block_state {
continue;
}
world.set_block_state(*pos, server_block_state);
// TODO: implement these two functions
// if is_colliding(player, *pos, server_block_state) {
// abs_snap_to(state.player_pos);
// }
}
for pos in to_remove {
self.server_state.remove(&pos);
}
}
}
/// 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 [`StartUseItemQueued::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() {
commands.entity(event.entity).insert(StartUseItemQueued {
hand: event.hand,
force_block: event.force_block,
});
}
}
/// 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, Debug)]
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 BlockStatePredictionHandler,
&HitResultComponent,
&LookDirection,
Option<&Mining>,
)>,
entity_id_query: Query<&MinecraftEntityId>,
) {
for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
query
{
commands.entity(entity).remove::<StartUseItemQueued>();
if mining.is_some() {
warn!("Got a StartUseItemEvent for a client that was mining");
}
// TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
// rowing a boat
let mut hit_result = (**hit_result).clone();
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,
});
}
}
match &hit_result {
HitResult::Block(r) => {
let seq = prediction_handler.start_predicting();
if r.miss {
commands.trigger(SendPacketEvent::new(
entity,
ServerboundUseItem {
hand: start_use_item.hand,
seq,
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: r.into(),
seq,
},
));
// 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(r) => {
// TODO: worldborder check
let Ok(entity_id) = entity_id_query.get(r.entity).copied() else {
warn!("tried to interact with an entity that doesn't have MinecraftEntityId");
continue;
};
commands.trigger(SendPacketEvent::new(
entity,
ServerboundInteract {
entity_id,
action: s_interact::ActionType::InteractAt {
location: r.location,
hand: InteractionHand::MainHand,
},
// TODO: sneaking
using_secondary_action: false,
},
));
}
}
}
}
/// Whether we can't interact with the block, based on your gamemode. If
/// this is false, then we can interact with the block.
///
/// Passing the inventory, block position, and instance is necessary for the
/// adventure mode check.
pub fn check_is_interaction_restricted(
instance: &Instance,
block_pos: BlockPos,
game_mode: &GameMode,
inventory: &Inventory,
) -> bool {
match game_mode {
GameMode::Adventure => {
// vanilla checks for abilities.mayBuild here but servers have no
// way of modifying that
let held_item = inventory.held_item();
match &held_item {
ItemStack::Present(item) => {
let block = instance.chunks.get_block_state(block_pos);
let Some(block) = block else {
// block isn't loaded so just say that it is restricted
return true;
};
check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
}
_ => true,
}
}
GameMode::Spectator => true,
_ => false,
}
}
/// Check if the item has the `CanDestroy` tag for the block.
pub fn check_block_can_be_broken_by_item_in_adventure_mode(
item: &ItemStackData,
_block: &BlockState,
) -> bool {
// minecraft caches the last checked block but that's kind of an unnecessary
// optimization and makes the code too complicated
if !item.components.has::<components::CanBreak>() {
// no CanDestroy tag
return false;
};
false
// for block_predicate in can_destroy {
// // TODO
// // defined in BlockPredicateArgument.java
// }
// true
}
pub fn can_use_game_master_blocks(
abilities: &PlayerAbilities,
permission_level: &PermissionLevel,
) -> bool {
abilities.instant_break && **permission_level >= 2
}
/// Swing your arm. This is purely a visual effect and won't interact with
/// anything in the world.
#[derive(Event, Clone, Debug)]
pub struct SwingArmEvent {
pub entity: Entity,
}
pub fn handle_swing_arm_trigger(trigger: Trigger<SwingArmEvent>, mut commands: Commands) {
commands.trigger(SendPacketEvent::new(
trigger.event().entity,
ServerboundSwing {
hand: InteractionHand::MainHand,
},
));
}
pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut commands: Commands) {
for event in events.read() {
commands.trigger(event.clone());
}
}
#[allow(clippy::type_complexity)]
fn update_attributes_for_held_item(
mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
) {
for (mut attributes, inventory) in &mut query {
let held_item = inventory.held_item();
use azalea_registry::Item;
let added_attack_speed = match held_item.kind() {
Item::WoodenSword => -2.4,
Item::WoodenShovel => -3.0,
Item::WoodenPickaxe => -2.8,
Item::WoodenAxe => -3.2,
Item::WoodenHoe => -3.0,
Item::StoneSword => -2.4,
Item::StoneShovel => -3.0,
Item::StonePickaxe => -2.8,
Item::StoneAxe => -3.2,
Item::StoneHoe => -2.0,
Item::GoldenSword => -2.4,
Item::GoldenShovel => -3.0,
Item::GoldenPickaxe => -2.8,
Item::GoldenAxe => -3.0,
Item::GoldenHoe => -3.0,
Item::IronSword => -2.4,
Item::IronShovel => -3.0,
Item::IronPickaxe => -2.8,
Item::IronAxe => -3.1,
Item::IronHoe => -1.0,
Item::DiamondSword => -2.4,
Item::DiamondShovel => -3.0,
Item::DiamondPickaxe => -2.8,
Item::DiamondAxe => -3.0,
Item::DiamondHoe => 0.0,
Item::NetheriteSword => -2.4,
Item::NetheriteShovel => -3.0,
Item::NetheritePickaxe => -2.8,
Item::NetheriteAxe => -3.0,
Item::NetheriteHoe => 0.0,
Item::Trident => -2.9,
_ => 0.,
};
attributes
.attack_speed
.insert(azalea_entity::attributes::base_attack_speed_modifier(
added_attack_speed,
));
}
}
#[allow(clippy::type_complexity)]
fn update_attributes_for_gamemode(
query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
) {
for (mut attributes, game_mode) in query {
if game_mode.current == GameMode::Creative {
attributes
.block_interaction_range
.insert(creative_block_interaction_range_modifier());
attributes
.entity_interaction_range
.insert(creative_entity_interaction_range_modifier());
} else {
attributes
.block_interaction_range
.remove(&creative_block_interaction_range_modifier().id);
attributes
.entity_interaction_range
.remove(&creative_entity_interaction_range_modifier().id);
}
}
}

View file

@ -0,0 +1,282 @@
use azalea_core::{
aabb::AABB,
direction::Direction,
hit_result::{BlockHitResult, EntityHitResult, HitResult},
position::Vec3,
};
use azalea_entity::{
Attributes, Dead, EyeHeight, LocalEntity, LookDirection, Physics, Position,
metadata::{ArmorStandMarker, Marker},
view_vector,
};
use azalea_physics::{
clip::{BlockShapeType, ClipContext, FluidPickType},
collision::entity_collisions::{PhysicsQuery, get_entities},
};
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
/// 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(HitResult);
#[allow(clippy::type_complexity)]
pub fn update_hit_result_component(
mut commands: Commands,
mut query: Query<
(
Entity,
Option<&mut HitResultComponent>,
&Position,
&EyeHeight,
&LookDirection,
&InstanceName,
&Physics,
&Attributes,
),
With<LocalEntity>,
>,
instance_container: Res<InstanceContainer>,
physics_query: PhysicsQuery,
pickable_query: PickableEntityQuery,
) {
for (
entity,
hit_result_ref,
position,
eye_height,
look_direction,
world_name,
physics,
attributes,
) in &mut query
{
let block_pick_range = attributes.block_interaction_range.calculate();
let entity_pick_range = attributes.entity_interaction_range.calculate();
let eye_position = position.up(eye_height.into());
let Some(world_lock) = instance_container.get(world_name) else {
continue;
};
let world = world_lock.read();
let hit_result = pick(PickOpts {
source_entity: entity,
look_direction: *look_direction,
eye_position,
aabb: &physics.bounding_box,
world: &world,
entity_pick_range,
block_pick_range,
physics_query: &physics_query,
pickable_query: &pickable_query,
});
if let Some(mut hit_result_ref) = hit_result_ref {
**hit_result_ref = hit_result;
} else {
commands
.entity(entity)
.insert(HitResultComponent(hit_result));
}
}
}
pub type PickableEntityQuery<'world, 'state, 'a> = Query<
'world,
'state,
Option<&'a ArmorStandMarker>,
(Without<Dead>, Without<Marker>, Without<LocalEntity>),
>;
pub struct PickOpts<'world, 'state, 'a, 'b, 'c> {
source_entity: Entity,
look_direction: LookDirection,
eye_position: Vec3,
aabb: &'a AABB,
world: &'a Instance,
entity_pick_range: f64,
block_pick_range: f64,
physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
pickable_query: &'a PickableEntityQuery<'world, 'state, 'c>,
}
/// 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`].
pub fn pick(opts: PickOpts<'_, '_, '_, '_, '_>) -> HitResult {
// vanilla does extra math here to calculate the pick result in between ticks by
// interpolating, but since clients can still only interact on exact ticks, that
// isn't relevant for us.
let mut max_range = opts.entity_pick_range.max(opts.block_pick_range);
let mut max_range_squared = max_range.powi(2);
let block_hit_result = pick_block(
opts.look_direction,
opts.eye_position,
&opts.world.chunks,
max_range,
);
let block_hit_result_dist_squared = block_hit_result
.location
.distance_squared_to(opts.eye_position);
if !block_hit_result.miss {
max_range_squared = block_hit_result_dist_squared;
max_range = block_hit_result_dist_squared.sqrt();
}
let view_vector = view_vector(opts.look_direction);
let end_position = opts.eye_position + (view_vector * max_range);
let inflate_by = 1.;
let pick_aabb = opts
.aabb
.expand_towards(view_vector * max_range)
.inflate_all(inflate_by);
let is_pickable = |entity: Entity| {
// TODO: ender dragon and projectiles have extra logic here. also, we shouldn't
// be able to pick spectators.
if let Ok(armor_stand_marker) = opts.pickable_query.get(entity) {
if let Some(armor_stand_marker) = armor_stand_marker
&& armor_stand_marker.0
{
false
} else {
true
}
} else {
true
}
};
let entity_hit_result = pick_entity(PickEntityOpts {
source_entity: opts.source_entity,
eye_position: opts.eye_position,
end_position,
world: opts.world,
pick_range_squared: max_range_squared,
predicate: &is_pickable,
aabb: &pick_aabb,
physics_query: opts.physics_query,
});
if let Some(entity_hit_result) = entity_hit_result
&& entity_hit_result
.location
.distance_squared_to(opts.eye_position)
< block_hit_result_dist_squared
{
filter_hit_result(
HitResult::Entity(entity_hit_result),
opts.eye_position,
opts.entity_pick_range,
)
} else {
filter_hit_result(
HitResult::Block(block_hit_result),
opts.eye_position,
opts.block_pick_range,
)
}
}
fn filter_hit_result(hit_result: HitResult, eye_position: Vec3, range: f64) -> HitResult {
let location = hit_result.location();
if !location.closer_than(eye_position, range) {
let direction = Direction::nearest(location - eye_position);
HitResult::new_miss(location, direction, location.into())
} else {
hit_result
}
}
/// Get the block that a player would be looking at if their eyes were at the
/// given direction and position.
///
/// Also see [`pick`].
pub fn pick_block(
look_direction: LookDirection,
eye_position: Vec3,
chunks: &azalea_world::ChunkStorage,
pick_range: f64,
) -> BlockHitResult {
let view_vector = view_vector(look_direction);
let end_position = eye_position + (view_vector * pick_range);
azalea_physics::clip::clip(
chunks,
ClipContext {
from: eye_position,
to: end_position,
block_shape_type: BlockShapeType::Outline,
fluid_pick_type: FluidPickType::None,
},
)
}
struct PickEntityOpts<'world, 'state, 'a, 'b> {
source_entity: Entity,
eye_position: Vec3,
end_position: Vec3,
world: &'a azalea_world::Instance,
pick_range_squared: f64,
predicate: &'a dyn Fn(Entity) -> bool,
aabb: &'a AABB,
physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
}
// port of getEntityHitResult
fn pick_entity(opts: PickEntityOpts) -> Option<EntityHitResult> {
let mut picked_distance_squared = opts.pick_range_squared;
let mut result = None;
for (candidate, candidate_aabb) in get_entities(
opts.world,
Some(opts.source_entity),
opts.aabb,
opts.predicate,
opts.physics_query,
) {
// TODO: if the entity is "REDIRECTABLE_PROJECTILE" then this should be 1.0.
// azalea needs support for entity tags first for this to be possible. see
// getPickRadius in decompiled minecraft source
let candidate_pick_radius = 0.;
let candidate_aabb = candidate_aabb.inflate_all(candidate_pick_radius);
let clip_location = candidate_aabb.clip(opts.eye_position, opts.end_position);
if candidate_aabb.contains(opts.eye_position) {
if picked_distance_squared >= 0. {
result = Some(EntityHitResult {
location: clip_location.unwrap_or(opts.eye_position),
entity: candidate,
});
picked_distance_squared = 0.;
}
} else if let Some(clip_location) = clip_location {
let distance_squared = opts.eye_position.distance_squared_to(clip_location);
if distance_squared < picked_distance_squared || picked_distance_squared == 0. {
// TODO: don't pick the entity we're riding on
// if candidate_root_vehicle == entity_root_vehicle {
// if picked_distance_squared == 0. {
// picked_entity = Some(candidate);
// picked_location = Some(clip_location);
// }
// } else {
result = Some(EntityHitResult {
location: clip_location,
entity: candidate,
});
picked_distance_squared = distance_squared;
}
}
}
result
}

View file

@ -1,4 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::{
cmp,
collections::{HashMap, HashSet},
};
use azalea_chat::FormattedText;
pub use azalea_inventory::*;
@ -16,14 +19,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::{
@ -67,13 +63,42 @@ impl Client {
let inventory = self.query::<&Inventory>(&mut ecs);
inventory.menu().clone()
}
/// Returns the index of the hotbar slot that's currently selected.
///
/// If you want to access the actual held item, you can get the current menu
/// with [`Client::menu`] and then get the slot index by offsetting from
/// the start of [`azalea_inventory::Menu::hotbar_slots_range`].
///
/// You can use [`Self::set_selected_hotbar_slot`] to change it.
pub fn selected_hotbar_slot(&self) -> u8 {
let mut ecs = self.ecs.lock();
let inventory = self.query::<&Inventory>(&mut ecs);
inventory.selected_hotbar_slot
}
/// Update the selected hotbar slot index.
///
/// This will run next `Update`, so you might want to call
/// `bot.wait_updates(1)` after calling this if you're using `azalea`.
pub fn set_selected_hotbar_slot(&self, new_hotbar_slot_index: u8) {
assert!(
new_hotbar_slot_index < 9,
"Hotbar slot index must be in the range 0..=8"
);
let mut ecs = self.ecs.lock();
ecs.send_event(SetSelectedHotbarSlotEvent {
entity: self.entity,
slot: new_hotbar_slot_index,
});
}
}
/// A component present on all local players that have an inventory.
#[derive(Component, Debug, Clone)]
pub struct Inventory {
/// A component that contains the player's inventory menu. This is
/// guaranteed to be a `Menu::Player`.
/// The player's inventory menu. This is guaranteed to be a `Menu::Player`.
///
/// We keep it as a [`Menu`] since `Menu` has some useful functions that
/// bare [`azalea_inventory::Player`] doesn't have.
@ -177,17 +202,17 @@ impl Inventory {
}
if let QuickCraftStatus::Add { slot } = quick_craft.status {
let slot_item = self.menu().slot(slot as usize);
if let Some(slot_item) = slot_item {
if let ItemStack::Present(carried) = &self.carried {
// minecraft also checks slot.may_place(carried) and
// menu.can_drag_to(slot)
// but they always return true so they're not relevant for us
if can_item_quick_replace(slot_item, &self.carried, true)
&& (self.quick_craft_kind == QuickCraftKind::Right
|| carried.count as usize > self.quick_craft_slots.len())
{
self.quick_craft_slots.insert(slot);
}
if let Some(slot_item) = slot_item
&& let ItemStack::Present(carried) = &self.carried
{
// minecraft also checks slot.may_place(carried) and
// menu.can_drag_to(slot)
// but they always return true so they're not relevant for us
if can_item_quick_replace(slot_item, &self.carried, true)
&& (self.quick_craft_kind == QuickCraftKind::Right
|| carried.count as usize > self.quick_craft_slots.len())
{
self.quick_craft_slots.insert(slot);
}
}
return;
@ -319,30 +344,95 @@ impl Inventory {
// player.drop(item, true);
}
}
ClickOperation::Pickup(
PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) },
&ClickOperation::Pickup(
// lol
ref pickup @ (PickupClick::Left { slot: Some(slot) }
| PickupClick::Right { slot: Some(slot) }),
) => {
let Some(slot_item) = self.menu().slot(*slot as usize) else {
let slot = slot as usize;
let Some(slot_item) = self.menu().slot(slot) else {
return;
};
let carried = &self.carried;
// vanilla does a check called tryItemClickBehaviourOverride
// here
// i don't understand it so i didn't implement it
if self.try_item_click_behavior_override(operation, slot) {
return;
}
let is_left_click = matches!(pickup, PickupClick::Left { .. });
match slot_item {
ItemStack::Empty => if carried.is_present() {},
ItemStack::Present(_) => todo!(),
ItemStack::Empty => {
if self.carried.is_present() {
let place_count = if is_left_click {
self.carried.count()
} else {
1
};
self.carried =
self.safe_insert(slot, self.carried.clone(), place_count);
}
}
ItemStack::Present(_) => {
if !self.menu().may_pickup(slot) {
return;
}
if let ItemStack::Present(carried) = self.carried.clone() {
let slot_is_same_item_as_carried = slot_item
.as_present()
.is_some_and(|s| carried.is_same_item_and_components(s));
if self.menu().may_place(slot, &carried) {
if slot_is_same_item_as_carried {
let place_count = if is_left_click { carried.count } else { 1 };
self.carried =
self.safe_insert(slot, self.carried.clone(), place_count);
} else if carried.count
<= self
.menu()
.max_stack_size(slot)
.min(carried.kind.max_stack_size())
{
// swap slot_item and carried
self.carried = slot_item.clone();
let slot_item = self.menu_mut().slot_mut(slot).unwrap();
*slot_item = carried.into();
}
} else if slot_is_same_item_as_carried
&& let Some(removed) = self.try_remove(
slot,
slot_item.count(),
carried.kind.max_stack_size() - carried.count,
)
{
self.carried.as_present_mut().unwrap().count += removed.count();
// slot.onTake(player, removed);
}
} else {
let pickup_count = if is_left_click {
slot_item.count()
} else {
(slot_item.count() + 1) / 2
};
if let Some(new_slot_item) =
self.try_remove(slot, pickup_count, i32::MAX)
{
self.carried = new_slot_item;
// slot.onTake(player, newSlot);
}
}
}
}
}
ClickOperation::QuickMove(
&ClickOperation::QuickMove(
QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
) => {
// in vanilla it also tests if QuickMove has a slot index of -999
// but i don't think that's ever possible so it's not covered here
let slot = slot as usize;
loop {
let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize);
let slot_item = self.menu().slot(*slot as usize).unwrap();
if new_slot_item.is_empty() || slot_item != &new_slot_item {
let new_slot_item = self.menu_mut().quick_move_stack(slot);
let slot_item = self.menu().slot(slot).unwrap();
if new_slot_item.is_empty() || slot_item.kind() != new_slot_item.kind() {
break;
}
}
@ -368,15 +458,16 @@ impl Inventory {
*target_slot = source_slot;
}
} else if source_slot.is_empty() {
let ItemStack::Present(target_item) = target_slot else {
unreachable!("target slot is not empty but is not present");
};
let target_item = target_slot
.as_present()
.expect("target slot was already checked to not be empty");
if self.menu().may_place(source_slot_index, target_item) {
// get the target_item but mutable
let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
let new_source_slot = target_slot.split(source_max_stack_size);
let new_source_slot =
target_slot.split(source_max_stack_size.try_into().unwrap());
*self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
}
} else if self.menu().may_pickup(source_slot_index) {
@ -385,11 +476,12 @@ impl Inventory {
};
if self.menu().may_place(source_slot_index, target_item) {
let source_max_stack = self.menu().max_stack_size(source_slot_index);
if target_slot.count() > source_max_stack as i32 {
if target_slot.count() > source_max_stack {
// if there's more than the max stack size in the target slot
let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
let new_source_slot = target_slot.split(source_max_stack);
let new_source_slot =
target_slot.split(source_max_stack.try_into().unwrap());
*self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
// if !self.inventory_menu.add(new_source_slot) {
// player.drop(new_source_slot, true);
@ -475,26 +567,23 @@ impl Inventory {
for i in iterator {
if target_slot_item.count < target_slot_item.kind.max_stack_size() {
let checking_slot = self.menu().slot(i).unwrap();
if let ItemStack::Present(checking_item) = checking_slot {
if can_item_quick_replace(checking_slot, &target_slot, true)
&& self.menu().may_pickup(i)
&& (round != 0
|| checking_item.count
!= checking_item.kind.max_stack_size())
{
// get the checking_slot and checking_item again but mutable
let checking_slot = self.menu_mut().slot_mut(i).unwrap();
if let ItemStack::Present(checking_item) = checking_slot
&& can_item_quick_replace(checking_slot, &target_slot, true)
&& self.menu().may_pickup(i)
&& (round != 0
|| checking_item.count != checking_item.kind.max_stack_size())
{
// get the checking_slot and checking_item again but mutable
let checking_slot = self.menu_mut().slot_mut(i).unwrap();
let taken_item =
checking_slot.split(checking_slot.count() as u32);
let taken_item = checking_slot.split(checking_slot.count() as u32);
// now extend the carried item
let target_slot = &mut self.carried;
let ItemStack::Present(target_slot_item) = target_slot else {
unreachable!("target slot is not empty but is not present");
};
target_slot_item.count += taken_item.count();
}
// now extend the carried item
let target_slot = &mut self.carried;
let ItemStack::Present(target_slot_item) = target_slot else {
unreachable!("target slot is not empty but is not present");
};
target_slot_item.count += taken_item.count();
}
}
}
@ -509,12 +598,74 @@ impl Inventory {
self.quick_craft_slots.clear();
}
/// Get the item in the player's hotbar that is currently being held.
/// Get the item in the player's hotbar that is currently being held in its
/// main hand.
pub fn held_item(&self) -> ItemStack {
let inventory = &self.inventory_menu;
let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
hotbar_items[self.selected_hotbar_slot as usize].clone()
}
/// TODO: implement bundles
fn try_item_click_behavior_override(
&self,
_operation: &ClickOperation,
_slot_item_index: usize,
) -> bool {
false
}
fn safe_insert(&mut self, slot: usize, src_item: ItemStack, take_count: i32) -> ItemStack {
let Some(slot_item) = self.menu_mut().slot_mut(slot) else {
return src_item;
};
let ItemStack::Present(mut src_item) = src_item else {
return src_item;
};
let take_count = cmp::min(
cmp::min(take_count, src_item.count),
src_item.kind.max_stack_size() - slot_item.count(),
);
if take_count <= 0 {
return src_item.into();
}
let take_count = take_count as u32;
if slot_item.is_empty() {
*slot_item = src_item.split(take_count).into();
} else if let ItemStack::Present(slot_item) = slot_item
&& slot_item.is_same_item_and_components(&src_item)
{
src_item.count -= take_count as i32;
slot_item.count += take_count as i32;
}
src_item.into()
}
fn try_remove(&mut self, slot: usize, count: i32, limit: i32) -> Option<ItemStack> {
if !self.menu().may_pickup(slot) {
return None;
}
let mut slot_item = self.menu().slot(slot)?.clone();
if !self.menu().allow_modification(slot) && limit < slot_item.count() {
return None;
}
let count = count.min(limit);
if count <= 0 {
return None;
}
// vanilla calls .remove here but i think it has the same behavior as split?
let removed = slot_item.split(count as u32);
if removed.is_present() && slot_item.is_empty() {
*self.menu_mut().slot_mut(slot).unwrap() = ItemStack::Empty;
}
Some(removed)
}
}
fn can_item_quick_replace(
@ -628,7 +779,7 @@ fn handle_container_close_event(
container_id: inventory.id,
},
));
client_side_events.send(ClientSideCloseContainerEvent {
client_side_events.write(ClientSideCloseContainerEvent {
entity: event.entity,
});
}
@ -647,7 +798,31 @@ pub fn handle_client_side_close_container_event(
) {
for event in events.read() {
let mut inventory = query.get_mut(event.entity).unwrap();
inventory.container_menu = None;
// copy the Player part of the container_menu to the inventory_menu
if let Some(inventory_menu) = inventory.container_menu.take() {
// this isn't the same as what vanilla does. i believe vanilla synchronizes the
// slots between inventoryMenu and containerMenu by just having the player slots
// point to the same ItemStack in memory, but emulating this in rust would
// require us to wrap our `ItemStack`s as `Arc<Mutex<ItemStack>>` which would
// have kinda terrible ergonomics.
// the simpler solution i chose to go with here is to only copy the player slots
// when the container is closed. this is perfectly fine for vanilla, but it
// might cause issues if a server modifies id 0 while we have a container
// open...
// if we do encounter this issue in the wild then the simplest solution would
// probably be to just add logic for updating the container_menu when the server
// tries to modify id 0 for slots within `inventory`. not implemented for now
// because i'm not sure if that's worth worrying about.
let new_inventory =
inventory_menu.slots()[inventory_menu.player_slots_range()].to_vec();
let new_inventory = <[ItemStack; 36]>::try_from(new_inventory).unwrap();
*inventory.inventory_menu.as_player_mut().inventory = new_inventory;
}
inventory.id = 0;
inventory.container_menu_title = None;
}
@ -660,12 +835,12 @@ pub struct ContainerClickEvent {
pub operation: ClickOperation,
}
pub fn handle_container_click_event(
mut query: Query<(Entity, &mut Inventory)>,
mut query: Query<(Entity, &mut Inventory, Option<&PlayerAbilities>)>,
mut events: EventReader<ContainerClickEvent>,
mut commands: Commands,
) {
for event in events.read() {
let (entity, mut inventory) = query.get_mut(event.entity).unwrap();
let (entity, mut inventory, player_abilities) = query.get_mut(event.entity).unwrap();
if inventory.id != event.window_id {
error!(
"Tried to click container with ID {}, but the current container ID is {}. Click packet won't be sent.",
@ -674,16 +849,18 @@ pub fn handle_container_click_event(
continue;
}
let menu = inventory.menu_mut();
let old_slots = menu.slots().clone();
// menu.click(&event.operation);
let old_slots = inventory.menu().slots();
inventory.simulate_click(
&event.operation,
player_abilities.unwrap_or(&PlayerAbilities::default()),
);
let new_slots = inventory.menu().slots();
// see which slots changed after clicking and put them in the hashmap
// the server uses this to check if we desynced
let mut changed_slots: HashMap<u16, HashedStack> = HashMap::new();
for (slot_index, old_slot) in old_slots.iter().enumerate() {
let new_slot = &menu.slots()[slot_index];
let new_slot = &new_slots[slot_index];
if old_slot != new_slot {
changed_slots.insert(slot_index as u16, HashedStack::from(new_slot));
}
@ -764,3 +941,49 @@ fn handle_set_selected_hotbar_slot_event(
));
}
}
#[cfg(test)]
mod tests {
use azalea_registry::Item;
use super::*;
#[test]
fn test_simulate_shift_click_in_crafting_table() {
let spruce_planks = ItemStack::Present(ItemStackData {
count: 4,
kind: Item::SprucePlanks,
components: Default::default(),
});
let mut inventory = Inventory {
inventory_menu: Menu::Player(azalea_inventory::Player::default()),
id: 1,
container_menu: Some(Menu::Crafting {
result: spruce_planks.clone(),
// simulate_click won't delete the items from here
grid: SlotList::default(),
player: SlotList::default(),
}),
container_menu_title: None,
carried: ItemStack::Empty,
state_id: 0,
quick_craft_status: QuickCraftStatusKind::Start,
quick_craft_kind: QuickCraftKind::Middle,
quick_craft_slots: HashSet::new(),
selected_hotbar_slot: 0,
};
inventory.simulate_click(
&ClickOperation::QuickMove(QuickMoveClick::Left { slot: 0 }),
&PlayerAbilities::default(),
);
let new_slots = inventory.menu().slots();
assert_eq!(&new_slots[0], &ItemStack::Empty);
assert_eq!(
&new_slots[*Menu::CRAFTING_PLAYER_SLOTS.start()],
&spruce_planks
);
}
}

View file

@ -0,0 +1,236 @@
use std::{net::SocketAddr, sync::Arc};
use azalea_entity::{LocalEntity, indexing::EntityUuidIndex};
use azalea_protocol::{
ServerAddress,
common::client_information::ClientInformation,
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, 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,
)
.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>>,
// this is mpsc instead of oneshot so it can be cloned (since it's sent in an event)
pub start_join_callback_tx: Option<mpsc::UnboundedSender<Entity>>,
}
/// 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,
}
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)
&& 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.send(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
};
if let Some(start_join_callback) = &event.start_join_callback_tx {
let _ = start_join_callback.send(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,
// this is inserted early so the user can always access and modify it
ClientInformation::default(),
// 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()));
}
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)>,
mut connection_failed_events: EventWriter<ConnectionFailedEvent>,
) {
for (entity, mut task, account) 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,
),
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(),
},
));
}
}
}

View file

@ -0,0 +1,51 @@
use azalea_core::tick::GameTick;
use azalea_entity::{InLoadedChunk, LocalEntity};
use azalea_physics::PhysicsSet;
use azalea_protocol::packets::game::ServerboundPlayerLoaded;
use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*;
use crate::{mining::MiningSet, packet::game::SendPacketEvent};
pub struct PlayerLoadedPlugin;
impl Plugin for PlayerLoadedPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
GameTick,
player_loaded_packet
.after(PhysicsSet)
.before(MiningSet)
.before(crate::movement::send_position),
);
}
}
// this component is removed on respawn or disconnect
// (notably, it's not removed on login)
// mojmap interchangeably calls it 'has client loaded' and 'has player loaded',
// i prefer the client one because it makes it clear that the component is only
// present on our own clients
#[derive(Component)]
pub struct HasClientLoaded;
#[allow(clippy::type_complexity)]
pub fn player_loaded_packet(
mut commands: Commands,
query: Query<
Entity,
(
With<LocalEntity>,
Without<HasClientLoaded>,
// the vanilla client waits for the chunk mesh to be "compiled" for the renderer (or
// some other conditions) before sending PlayerLoaded. see LevelLoadStatusManager.tick
// in the decompiled source
With<InLoadedChunk>,
),
>,
) {
for entity in query.iter() {
commands.trigger(SendPacketEvent::new(entity, ServerboundPlayerLoaded));
commands.entity(entity).insert(HasClientLoaded);
}
}

View file

@ -0,0 +1,171 @@
use azalea_auth::sessionserver::ClientSessionServerError;
use azalea_protocol::packets::login::{
ClientboundHello, ServerboundCustomQueryAnswer, ServerboundKey,
};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_tasks::{IoTaskPool, Task, futures_lite::future};
use thiserror::Error;
use tracing::{debug, error, trace};
use super::{
connection::RawConnection,
packet::login::{ReceiveCustomQueryEvent, ReceiveHelloEvent, SendLoginPacketEvent},
};
use crate::Account;
/// Some systems that run during the `login` state.
pub struct LoginPlugin;
impl Plugin for LoginPlugin {
fn build(&self, app: &mut App) {
app.add_observer(handle_receive_hello_event)
.add_systems(Update, (poll_auth_task, reply_to_custom_queries));
}
}
fn handle_receive_hello_event(trigger: Trigger<ReceiveHelloEvent>, mut commands: Commands) {
let task_pool = IoTaskPool::get();
let account = trigger.account.clone();
let packet = trigger.packet.clone();
let player = trigger.target();
let task = task_pool.spawn(auth_with_account(account, packet));
commands.entity(player).insert(AuthTask(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>()
.insert(IsAuthenticated);
match poll_res {
Ok((packet, private_key)) => {
// we use this instead of SendLoginPacketEvent to ensure that it's sent right
// before encryption is enabled. i guess another option would be to make a
// Trigger+observer for set_encryption_key; the current implementation is
// simpler though.
if let Err(e) = raw_conn.write(packet) {
error!("Error sending key packet: {e:?}");
}
if let Some(net_conn) = raw_conn.net_conn() {
net_conn.set_encryption_key(private_key);
}
}
Err(err) => {
error!("Error during authentication: {err:?}");
}
}
}
}
}
type PrivateKey = [u8; 16];
#[derive(Component)]
pub struct AuthTask(Task<Result<(ServerboundKey, PrivateKey), AuthWithAccountError>>);
#[derive(Debug, Error)]
pub enum AuthWithAccountError {
#[error("Failed to encrypt the challenge from the server for {0:?}")]
Encryption(ClientboundHello),
#[error("{0}")]
SessionServer(#[from] ClientSessionServerError),
#[error("Couldn't refresh access token: {0}")]
Auth(#[from] azalea_auth::AuthError),
}
pub async fn auth_with_account(
account: Account,
packet: ClientboundHello,
) -> Result<(ServerboundKey, PrivateKey), AuthWithAccountError> {
let Ok(encrypt_res) = azalea_crypto::encrypt(&packet.public_key, &packet.challenge) else {
return Err(AuthWithAccountError::Encryption(packet));
};
let key_packet = ServerboundKey {
key_bytes: encrypt_res.encrypted_public_key,
encrypted_challenge: encrypt_res.encrypted_challenge,
};
let private_key = encrypt_res.secret_key;
let Some(access_token) = &account.access_token else {
// offline mode account, no need to do auth
return Ok((key_packet, private_key));
};
// keep track of the number of times we tried authenticating so we can give up
// after too many
let mut attempts: usize = 1;
while let Err(err) = {
let access_token = access_token.lock().clone();
let uuid = &account
.uuid
.expect("Uuid must be present if access token is present.");
// this is necessary since reqwest usually depends on tokio and we're using
// `futures` here
async_compat::Compat::new(async {
azalea_auth::sessionserver::join(
&access_token,
&packet.public_key,
&private_key,
uuid,
&packet.server_id,
)
.await
})
.await
} {
if attempts >= 2 {
// if this is the second attempt and we failed
// both times, give up
return Err(err.into());
}
if matches!(
err,
ClientSessionServerError::InvalidSession | ClientSessionServerError::ForbiddenOperation
) {
// uh oh, we got an invalid session and have
// to reauthenticate now
account.refresh().await?;
} else {
return Err(err.into());
}
attempts += 1;
}
Ok((key_packet, private_key))
}
pub fn reply_to_custom_queries(
mut commands: Commands,
mut events: EventReader<ReceiveCustomQueryEvent>,
) {
for event in events.read() {
trace!("Maybe replying to custom query: {event:?}");
if event.disabled {
continue;
}
commands.trigger(SendLoginPacketEvent::new(
event.entity,
ServerboundCustomQueryAnswer {
transaction_id: event.packet.transaction_id,
data: None,
},
));
}
}

View file

@ -1,22 +1,23 @@
use azalea_block::{Block, BlockState, fluid_state::FluidState};
use azalea_block::{BlockState, BlockTrait, 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_entity::{FluidOnEyes, Physics, Position, 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};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use tracing::trace;
use crate::{
Client,
interact::{
CurrentSequenceNumber, HitResultComponent, SwingArmEvent, can_use_game_master_blocks,
check_is_interaction_restricted,
BlockStatePredictionHandler, SwingArmEvent, can_use_game_master_blocks,
check_is_interaction_restricted, pick::HitResultComponent,
},
inventory::{Inventory, InventorySet},
local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
local_player::{InstanceHolder, LocalGameMode, PermissionLevel, PlayerAbilities},
movement::MoveEventsSet,
packet::game::SendPacketEvent,
};
@ -26,23 +27,28 @@ 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),
.after(PhysicsSet)
.after(super::movement::send_position)
.after(super::attack::handle_attack_queued)
.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,
)
.chain()
@ -51,11 +57,11 @@ impl Plugin for MiningPlugin {
.after(MoveEventsSet)
.before(azalea_entity::update_bounding_box)
.after(azalea_entity::update_fluid_on_eyes)
.after(crate::interact::update_hit_result_component)
.after(crate::interact::pick::update_hit_result_component)
.after(crate::attack::handle_attack_event)
.after(crate::interact::handle_block_interact_event)
.before(crate::interact::handle_swing_arm_event),
);
)
.add_observer(handle_finish_mining_block_observer);
}
}
@ -65,7 +71,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,116 +124,128 @@ 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.miss() {
stop_mining_block_event.write(StopMiningBlockEvent { entity });
}
}
}
/// Information about the block we're currently mining. This is only present if
/// we're currently mining a block.
#[derive(Component)]
#[derive(Component, Debug, Clone)]
pub struct Mining {
pub pos: BlockPos,
pub dir: Direction,
/// See [`MiningQueued::force`].
pub force: bool,
}
/// Start mining the block at the given position.
///
/// If we're looking at the block then the correct direction will be used,
/// otherwise it'll be [`Direction::Down`].
#[derive(Event)]
#[derive(Event, Debug)]
pub struct StartMiningBlockEvent {
pub entity: Entity,
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() {
trace!("{event:?}");
let hit_result = query.get_mut(event.entity).unwrap();
let direction = if hit_result.block_pos == event.position {
let (direction, force) = 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, false)
} else {
// we're not looking at the block, arbitrary direction
Direction::Down
(Direction::Down, true)
};
start_mining_events.send(StartMiningBlockWithDirectionEvent {
entity: event.entity,
commands.entity(event.entity).insert(MiningQueued {
position: event.position,
direction,
force,
});
}
}
#[derive(Event)]
pub struct StartMiningBlockWithDirectionEvent {
pub entity: Entity,
/// Present on entities when they're going to start mining a block next tick.
#[derive(Component, Debug, Clone)]
pub struct MiningQueued {
pub position: BlockPos,
pub direction: Direction,
/// Whether we should mine the block regardless of whether it's reachable.
pub force: bool,
}
#[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 attack_block_events: EventWriter<AttackBlockEvent>,
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
mut query: Query<(
&InstanceName,
query: Query<(
Entity,
&MiningQueued,
&InstanceHolder,
&LocalGameMode,
&Inventory,
&FluidOnEyes,
&Physics,
Option<&Mining>,
&mut CurrentSequenceNumber,
&mut BlockStatePredictionHandler,
&mut MineDelay,
&mut MineProgress,
&mut MineTicks,
&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 +255,16 @@ 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,
});
commands.trigger_targets(
FinishMiningBlockEvent {
position: mining_queued.position,
},
entity,
);
**mine_delay = 5;
} else if mining.is_none()
|| !is_same_mining_target(
event.position,
mining_queued.position,
inventory,
&current_mining_pos,
&current_mining_item,
@ -252,44 +273,33 @@ 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,
sequence: 0,
direction: mining_queued.direction,
seq: 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,
});
}
let block = Box::<dyn Block>::from(target_block_state);
let block = Box::<dyn BlockTrait>::from(target_block_state);
let held_item = inventory.held_item();
@ -302,36 +312,44 @@ fn handle_start_mining_block_with_direction_event(
physics,
) >= 1.
{
// block was broken instantly
finish_mining_events.send(FinishMiningBlockEvent {
entity: event.entity,
position: event.position,
});
// block was broken instantly (instamined)
commands.trigger_targets(
FinishMiningBlockEvent {
position: mining_queued.position,
},
entity,
);
} else {
commands.entity(event.entity).insert(Mining {
pos: event.position,
dir: event.direction,
});
**current_mining_pos = Some(event.position);
let mining = Mining {
pos: mining_queued.position,
dir: mining_queued.direction,
force: mining_queued.force,
};
trace!("inserting mining component {mining:?} for entity {entity:?}");
commands.entity(entity).insert(mining);
**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,
seq: sequence_number.start_predicting(),
},
));
// vanilla really does send two swing arm packets
commands.trigger(SwingArmEvent { entity });
commands.trigger(SwingArmEvent { entity });
}
}
}
@ -364,7 +382,7 @@ fn is_same_mining_target(
}
/// A component bundle for players that can mine blocks.
#[derive(Bundle, Default)]
#[derive(Bundle, Default, Clone)]
pub struct MineBundle {
pub delay: MineDelay,
pub progress: MineProgress,
@ -374,12 +392,12 @@ pub struct MineBundle {
}
/// A component that counts down until we start mining the next block.
#[derive(Component, Debug, Default, Deref, DerefMut)]
#[derive(Component, Debug, Default, Deref, DerefMut, Clone)]
pub struct MineDelay(pub u32);
/// A component that stores the progress of the current mining operation. This
/// is a value between 0 and 1.
#[derive(Component, Debug, Default, Deref, DerefMut)]
#[derive(Component, Debug, Default, Deref, DerefMut, Clone)]
pub struct MineProgress(pub f32);
impl MineProgress {
@ -407,72 +425,78 @@ pub struct MineBlockPos(pub Option<BlockPos>);
#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
pub struct MineItem(pub ItemStack);
/// Sent when we completed mining a block.
/// A trigger that's sent when we completed mining a block.
#[derive(Event)]
pub struct FinishMiningBlockEvent {
pub entity: Entity,
pub position: BlockPos,
}
pub fn handle_finish_mining_block_event(
mut events: EventReader<FinishMiningBlockEvent>,
pub fn handle_finish_mining_block_observer(
trigger: Trigger<FinishMiningBlockEvent>,
mut query: Query<(
&InstanceName,
&LocalGameMode,
&Inventory,
&PlayerAbilities,
&PermissionLevel,
&mut CurrentSequenceNumber,
&Position,
&mut BlockStatePredictionHandler,
)>,
instances: Res<InstanceContainer>,
) {
for event in events.read() {
let (instance_name, game_mode, inventory, abilities, permission_level, _sequence_number) =
query.get_mut(event.entity).unwrap();
let instance_lock = instances.get(instance_name).unwrap();
let instance = instance_lock.read();
if check_is_interaction_restricted(
&instance,
&event.position,
&game_mode.current,
inventory,
) {
continue;
}
let event = trigger.event();
if game_mode.current == GameMode::Creative {
let held_item = inventory.held_item().kind();
if matches!(
held_item,
azalea_registry::Item::Trident | azalea_registry::Item::DebugStick
) || azalea_registry::tags::items::SWORDS.contains(&held_item)
{
continue;
}
}
let Some(block_state) = instance.get_block_state(&event.position) else {
continue;
};
let registry_block = Box::<dyn Block>::from(block_state).as_registry_block();
if !can_use_game_master_blocks(abilities, permission_level)
&& matches!(
registry_block,
azalea_registry::Block::CommandBlock | azalea_registry::Block::StructureBlock
)
{
continue;
}
if block_state == BlockState::AIR {
continue;
}
// when we break a waterlogged block we want to keep the water there
let fluid_state = FluidState::from(block_state);
let block_state_for_fluid = BlockState::from(fluid_state);
instance.set_block_state(&event.position, block_state_for_fluid);
let (
instance_name,
game_mode,
inventory,
abilities,
permission_level,
player_pos,
mut prediction_handler,
) = query.get_mut(trigger.target()).unwrap();
let instance_lock = instances.get(instance_name).unwrap();
let instance = instance_lock.read();
if check_is_interaction_restricted(&instance, event.position, &game_mode.current, inventory) {
return;
}
if game_mode.current == GameMode::Creative {
let held_item = inventory.held_item().kind();
if matches!(
held_item,
azalea_registry::Item::Trident | azalea_registry::Item::DebugStick
) || azalea_registry::tags::items::SWORDS.contains(&held_item)
{
return;
}
}
let Some(block_state) = instance.get_block_state(event.position) else {
return;
};
let registry_block: azalea_registry::Block =
Box::<dyn BlockTrait>::from(block_state).as_registry_block();
if !can_use_game_master_blocks(abilities, permission_level)
&& matches!(
registry_block,
azalea_registry::Block::CommandBlock | azalea_registry::Block::StructureBlock
)
{
return;
}
if block_state == BlockState::AIR {
return;
}
// when we break a waterlogged block we want to keep the water there
let fluid_state = FluidState::from(block_state);
let block_state_for_fluid = BlockState::from(fluid_state);
let old_state = instance
.set_block_state(event.position, block_state_for_fluid)
.unwrap_or_default();
prediction_handler.retain_known_server_state(event.position, old_state, **player_pos);
}
/// Abort mining a block.
@ -484,10 +508,10 @@ pub fn handle_stop_mining_block_event(
mut events: EventReader<StopMiningBlockEvent>,
mut commands: Commands,
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>,
mut query: Query<(&MineBlockPos, &mut MineProgress)>,
) {
for event in events.read() {
let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
let (mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
let mine_block_pos =
mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
@ -497,12 +521,12 @@ pub fn handle_stop_mining_block_event(
action: s_player_action::Action::AbortDestroyBlock,
pos: mine_block_pos,
direction: Direction::Down,
sequence: 0,
seq: 0,
},
));
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,
@ -525,13 +549,10 @@ pub fn continue_mining_block(
&mut MineDelay,
&mut MineProgress,
&mut MineTicks,
&mut CurrentSequenceNumber,
&mut BlockStatePredictionHandler,
)>,
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 (
@ -547,7 +568,7 @@ pub fn continue_mining_block(
mut mine_delay,
mut mine_progress,
mut mine_ticks,
mut sequence_number,
mut prediction_handler,
) in query.iter_mut()
{
if **mine_delay > 0 {
@ -558,36 +579,42 @@ pub fn continue_mining_block(
if game_mode.current == GameMode::Creative {
// TODO: worldborder check
**mine_delay = 5;
finish_mining_events.send(FinishMiningBlockEvent {
commands.trigger_targets(
FinishMiningBlockEvent {
position: mining.pos,
},
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,
seq: prediction_handler.start_predicting(),
},
));
swing_arm_events.send(SwingArmEvent { entity });
} else if is_same_mining_target(
mining.pos,
inventory,
current_mining_pos,
current_mining_item,
) {
commands.trigger(SwingArmEvent { entity });
} else if mining.force
|| is_same_mining_target(
mining.pos,
inventory,
current_mining_pos,
current_mining_item,
)
{
trace!("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();
let target_block_state = instance.get_block_state(mining.pos).unwrap_or_default();
trace!("target_block_state: {target_block_state:?}");
if target_block_state.is_air() {
commands.entity(entity).remove::<Mining>();
continue;
}
let block = Box::<dyn Block>::from(target_block_state);
let block = Box::<dyn BlockTrait>::from(target_block_state);
**mine_progress += get_mine_progress(
block.as_ref(),
current_mining_item.kind(),
@ -602,40 +629,57 @@ pub fn continue_mining_block(
**mine_ticks += 1.;
if **mine_progress >= 1. {
commands.entity(entity).remove::<Mining>();
*sequence_number += 1;
finish_mining_events.send(FinishMiningBlockEvent {
// MiningQueued is removed in case we were doing an infinite loop that
// repeatedly inserts MiningQueued
commands.entity(entity).remove::<(Mining, MiningQueued)>();
trace!("finished mining block at {:?}", mining.pos);
commands.trigger_targets(
FinishMiningBlockEvent {
position: mining.pos,
},
entity,
position: mining.pos,
});
);
commands.trigger(SendPacketEvent::new(
entity,
ServerboundPlayerAction {
action: s_player_action::Action::StopDestroyBlock,
pos: mining.pos,
direction: mining.dir,
sequence: **sequence_number,
seq: prediction_handler.start_predicting(),
},
));
**mine_progress = 0.;
**mine_ticks = 0.;
**mine_delay = 0;
**mine_delay = 5;
}
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,
trace!("switching mining target to {:?}", mining.pos);
commands.entity(entity).insert(MiningQueued {
position: mining.pos,
direction: mining.dir,
force: false,
});
}
swing_arm_events.send(SwingArmEvent { entity });
}
}
pub fn update_mining_component(
mut commands: Commands,
mut query: Query<(Entity, &mut Mining, &HitResultComponent)>,
) {
for (entity, mut mining, hit_result_component) in &mut query.iter_mut() {
if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() {
mining.pos = block_hit_result.block_pos;
mining.dir = block_hit_result.direction;
} else {
commands.entity(entity).remove::<Mining>();
}
}
}

View file

@ -1,15 +1,71 @@
use bevy_app::{PluginGroup, PluginGroupBuilder};
pub mod attack;
pub mod auto_reconnect;
pub mod block_update;
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 loading;
pub mod login;
pub mod mining;
pub mod movement;
pub mod packet;
pub mod pong;
pub mod respawn;
pub mod task_pool;
pub mod tick_broadcast;
pub mod tick_counter;
pub mod tick_end;
/// This plugin group will add all the default plugins necessary for Azalea to
/// work.
pub struct DefaultPlugins;
impl PluginGroup for DefaultPlugins {
fn build(self) -> PluginGroupBuilder {
#[allow(unused_mut)]
let mut group = PluginGroupBuilder::start::<Self>()
.add(crate::client::AmbiguityLoggerPlugin)
.add(bevy_time::TimePlugin)
.add(packet::PacketPlugin)
.add(crate::client::AzaleaPlugin)
.add(azalea_entity::EntityPlugin)
.add(azalea_physics::PhysicsPlugin)
.add(events::EventsPlugin)
.add(task_pool::TaskPoolPlugin::default())
.add(inventory::InventoryPlugin)
.add(chat::ChatPlugin)
.add(disconnect::DisconnectPlugin)
.add(movement::MovementPlugin)
.add(interact::InteractPlugin)
.add(respawn::RespawnPlugin)
.add(mining::MiningPlugin)
.add(attack::AttackPlugin)
.add(chunks::ChunksPlugin)
.add(block_update::BlockUpdatePlugin)
.add(tick_end::TickEndPlugin)
.add(loading::PlayerLoadedPlugin)
.add(brand::BrandPlugin)
.add(tick_broadcast::TickBroadcastPlugin)
.add(tick_counter::TickCounterPlugin)
.add(pong::PongPlugin)
.add(connection::ConnectionPlugin)
.add(login::LoginPlugin)
.add(join::JoinPlugin)
.add(auto_reconnect::AutoReconnectPlugin)
.add(chat_signing::ChatSigningPlugin);
#[cfg(feature = "log")]
{
group = group.add(bevy_log::LogPlugin::default());
}
group
}
}

View file

@ -1,40 +1,40 @@
use std::backtrace::Backtrace;
use std::{backtrace::Backtrace, io};
use azalea_core::position::Vec3;
use azalea_core::tick::GameTick;
use azalea_entity::{Attributes, Jumping, metadata::Sprinting};
use azalea_entity::{InLoadedChunk, LastSentPosition, LookDirection, Physics, Position};
use azalea_core::{
position::{Vec2, Vec3},
tick::GameTick,
};
use azalea_entity::{
Attributes, InLoadedChunk, Jumping, LastSentPosition, LookDirection, Physics, Position,
metadata::Sprinting,
};
use azalea_physics::{PhysicsSet, ai_step};
use azalea_protocol::packets::game::{ServerboundPlayerCommand, ServerboundPlayerInput};
use azalea_protocol::packets::{
Packet,
game::{
s_move_player_pos::ServerboundMovePlayerPos,
s_move_player_pos_rot::ServerboundMovePlayerPosRot,
s_move_player_rot::ServerboundMovePlayerRot,
s_move_player_status_only::ServerboundMovePlayerStatusOnly,
use azalea_protocol::{
common::movements::MoveFlags,
packets::{
Packet,
game::{
ServerboundPlayerCommand, ServerboundPlayerInput,
s_move_player_pos::ServerboundMovePlayerPos,
s_move_player_pos_rot::ServerboundMovePlayerPosRot,
s_move_player_rot::ServerboundMovePlayerRot,
s_move_player_status_only::ServerboundMovePlayerStatusOnly,
},
},
};
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;
use crate::packet::game::SendPacketEvent;
use crate::{client::Client, packet::game::SendPacketEvent};
#[derive(Error, Debug)]
pub enum MovePlayerError {
#[error("Player is not in world")]
PlayerNotInWorld(Backtrace),
#[error("{0}")]
Io(#[from] std::io::Error),
Io(#[from] io::Error),
}
impl From<MoveEntityError> for MovePlayerError {
@ -68,8 +68,8 @@ impl Plugin for MovementPlugin {
.in_set(PhysicsSet)
.before(ai_step)
.before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk),
send_player_input_packet,
send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk),
send_position.after(PhysicsSet),
)
.chain(),
@ -140,8 +140,7 @@ pub struct PhysicsState {
pub trying_to_sprint: bool,
pub move_direction: WalkDirection,
pub forward_impulse: f32,
pub left_impulse: f32,
pub move_vector: Vec2,
}
#[allow(clippy::type_complexity)]
@ -192,12 +191,16 @@ pub fn send_position(
// if self.is_passenger() {
// TODO: posrot packet for being a passenger
// }
let flags = MoveFlags {
on_ground: physics.on_ground(),
horizontal_collision: physics.horizontal_collision,
};
let packet = if sending_position && sending_direction {
Some(
ServerboundMovePlayerPosRot {
pos: **position,
look_direction: *direction,
on_ground: physics.on_ground(),
flags,
}
.into_variant(),
)
@ -205,7 +208,7 @@ pub fn send_position(
Some(
ServerboundMovePlayerPos {
pos: **position,
on_ground: physics.on_ground(),
flags,
}
.into_variant(),
)
@ -213,17 +216,12 @@ pub fn send_position(
Some(
ServerboundMovePlayerRot {
look_direction: *direction,
on_ground: physics.on_ground(),
flags,
}
.into_variant(),
)
} else if physics.last_on_ground() != physics.on_ground() {
Some(
ServerboundMovePlayerStatusOnly {
on_ground: physics.on_ground(),
}
.into_variant(),
)
Some(ServerboundMovePlayerStatusOnly { flags }.into_variant())
} else {
None
};
@ -312,12 +310,10 @@ pub fn send_sprinting_if_needed(
}
}
/// Update the impulse from self.move_direction. The multiplier is used for
/// sneaking.
/// Updates the [`PhysicsState::move_vector`] based on the
/// [`PhysicsState::move_direction`].
pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
for mut physics_state in query.iter_mut() {
let multiplier: Option<f32> = None;
let mut forward_impulse: f32 = 0.;
let mut left_impulse: f32 = 0.;
let move_direction = physics_state.move_direction;
@ -341,13 +337,9 @@ pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
}
_ => {}
};
physics_state.forward_impulse = forward_impulse;
physics_state.left_impulse = left_impulse;
if let Some(multiplier) = multiplier {
physics_state.forward_impulse *= multiplier;
physics_state.left_impulse *= multiplier;
}
let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
physics_state.move_vector = move_vector;
}
}
@ -361,8 +353,12 @@ pub fn local_player_ai_step(
) {
for (physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() {
// server ai step
physics.x_acceleration = physics_state.left_impulse;
physics.z_acceleration = physics_state.forward_impulse;
// TODO: replace those booleans when using items, passengers, and sneaking are
// properly implemented
let move_vector = modify_input(physics_state.move_vector, false, false, false, &attributes);
physics.x_acceleration = move_vector.x;
physics.z_acceleration = move_vector.y;
// TODO: food data and abilities
// let has_enough_food_to_sprint = self.food_data().food_level ||
@ -389,6 +385,47 @@ pub fn local_player_ai_step(
}
}
// LocalPlayer.modifyInput
fn modify_input(
mut move_vector: Vec2,
is_using_item: bool,
is_passenger: bool,
moving_slowly: bool,
attributes: &Attributes,
) -> Vec2 {
if move_vector.length_squared() == 0. {
return move_vector;
}
move_vector *= 0.98;
if is_using_item && !is_passenger {
move_vector *= 0.2;
}
if moving_slowly {
let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
move_vector *= sneaking_speed;
}
modify_input_speed_for_square_movement(move_vector)
}
fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
let length = move_vector.length();
if length == 0. {
return move_vector;
}
let scaled_to_inverse_length = move_vector * (1. / length);
let dist = distance_to_unit_square(scaled_to_inverse_length);
let scale = (length * dist).min(1.);
scaled_to_inverse_length * scale
}
fn distance_to_unit_square(v: Vec2) -> f32 {
let x = v.x.abs();
let y = v.y.abs();
let ratio = if y > x { x / y } else { y / x };
(1.0 + ratio * ratio).sqrt()
}
impl Client {
/// Start walking in the given direction. To sprint, use
/// [`Client::sprint`]. To stop walking, call walk with
@ -512,7 +549,7 @@ fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
// if self.underwater() {
// self.has_forward_impulse()
// } else {
physics_state.forward_impulse > 0.8
physics_state.move_vector.y > 0.8
// }
}

View file

@ -1,23 +1,20 @@
use std::io::Cursor;
use std::sync::Arc;
use azalea_protocol::{
packets::{
Packet,
config::{ClientboundConfigPacket, ServerboundConfigPacket},
},
read::deserialize_packet,
use azalea_protocol::packets::{
Packet,
config::{ClientboundConfigPacket, ServerboundConfigPacket},
};
use bevy_ecs::prelude::*;
use tracing::{debug, error};
use crate::{InConfigState, raw_connection::RawConnection};
use crate::{InConfigState, connection::RawConnection};
#[derive(Event, Debug, Clone)]
pub struct ReceiveConfigPacketEvent {
/// The client entity that received the packet.
pub entity: Entity,
/// The packet that was actually received.
pub packet: ClientboundConfigPacket,
pub packet: Arc<ClientboundConfigPacket>,
}
/// An event for sending a packet to the server while we're in the
@ -39,7 +36,7 @@ pub fn handle_outgoing_packets_observer(
mut query: Query<(&mut RawConnection, Option<&InConfigState>)>,
) {
let event = trigger.event();
if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) {
if let Ok((mut raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) {
if in_configuration_state.is_none() {
error!(
"Tried to send a configuration packet {:?} while not in configuration state",
@ -47,8 +44,8 @@ pub fn handle_outgoing_packets_observer(
);
return;
}
debug!("Sending packet: {:?}", event.packet);
if let Err(e) = raw_conn.write_packet(event.packet.clone()) {
debug!("Sending config packet: {:?}", event.packet);
if let Err(e) = raw_conn.write(event.packet.clone()) {
error!("Failed to send packet: {e}");
}
}
@ -64,61 +61,6 @@ pub fn handle_outgoing_packets(
}
}
pub fn emit_receive_config_packet_events(
query: Query<(Entity, &RawConnection), With<InConfigState>>,
mut packet_events: ResMut<Events<ReceiveConfigPacketEvent>>,
) {
// we manually clear and send the events at the beginning of each update
// since otherwise it'd cause issues with events in process_packet_events
// running twice
packet_events.clear();
for (player_entity, raw_conn) in &query {
let packets_lock = raw_conn.incoming_packet_queue();
let mut packets = packets_lock.lock();
if !packets.is_empty() {
let mut packets_read = 0;
for raw_packet in packets.iter() {
packets_read += 1;
let packet = match deserialize_packet::<ClientboundConfigPacket>(&mut Cursor::new(
raw_packet,
)) {
Ok(packet) => packet,
Err(err) => {
error!("failed to read packet: {err:?}");
debug!("packet bytes: {raw_packet:?}");
continue;
}
};
let should_interrupt = packet_interrupts(&packet);
packet_events.send(ReceiveConfigPacketEvent {
entity: player_entity,
packet,
});
if should_interrupt {
break;
}
}
packets.drain(0..packets_read);
}
}
}
/// Whether the given packet should make us stop deserializing the received
/// packets until next update.
///
/// This is used for packets that can switch the client state.
fn packet_interrupts(packet: &ClientboundConfigPacket) -> bool {
matches!(
packet,
ClientboundConfigPacket::FinishConfiguration(_)
| ClientboundConfigPacket::Disconnect(_)
| ClientboundConfigPacket::Transfer(_)
)
}
/// A Bevy trigger that's sent when our client receives a [`ClientboundPing`]
/// packet in the config state.
///

View file

@ -1,65 +1,65 @@
mod events;
use std::io::Cursor;
use azalea_entity::LocalEntity;
use azalea_protocol::packets::ConnectionProtocol;
use azalea_protocol::packets::config::*;
use azalea_protocol::{
packets::{ConnectionProtocol, config::*},
read::{ReadPacketError, deserialize_packet},
};
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemState;
pub use events::*;
use tracing::{debug, warn};
use super::as_system;
use crate::client::InConfigState;
use crate::disconnect::DisconnectEvent;
use crate::packet::game::KeepAliveEvent;
use crate::packet::game::ResourcePackEvent;
use crate::raw_connection::RawConnection;
use crate::{InstanceHolder, declare_packet_handlers};
use crate::{
client::InConfigState,
connection::RawConnection,
declare_packet_handlers,
disconnect::DisconnectEvent,
local_player::InstanceHolder,
packet::game::{KeepAliveEvent, ResourcePackEvent},
};
pub fn process_packet_events(ecs: &mut World) {
let mut events_owned = Vec::new();
let mut system_state: SystemState<EventReader<ReceiveConfigPacketEvent>> =
SystemState::new(ecs);
let mut events = system_state.get_mut(ecs);
for ReceiveConfigPacketEvent {
entity: player_entity,
pub fn process_raw_packet(
ecs: &mut World,
player: Entity,
raw_packet: &[u8],
) -> Result<(), Box<ReadPacketError>> {
let packet = deserialize_packet(&mut Cursor::new(raw_packet))?;
process_packet(ecs, player, &packet);
Ok(())
}
pub fn process_packet(ecs: &mut World, player: Entity, packet: &ClientboundConfigPacket) {
let mut handler = ConfigPacketHandler { player, ecs };
declare_packet_handlers!(
ClientboundConfigPacket,
packet,
} in events.read()
{
// we do this so `ecs` isn't borrowed for the whole loop
events_owned.push((*player_entity, packet.clone()));
}
for (player_entity, packet) in events_owned {
let mut handler = ConfigPacketHandler {
player: player_entity,
ecs,
};
declare_packet_handlers!(
ClientboundConfigPacket,
packet,
handler,
[
cookie_request,
custom_payload,
disconnect,
finish_configuration,
keep_alive,
ping,
reset_chat,
registry_data,
resource_pack_pop,
resource_pack_push,
store_cookie,
transfer,
update_enabled_features,
update_tags,
select_known_packs,
custom_report_details,
server_links,
]
);
}
handler,
[
cookie_request,
custom_payload,
disconnect,
finish_configuration,
keep_alive,
ping,
reset_chat,
registry_data,
resource_pack_pop,
resource_pack_push,
store_cookie,
transfer,
update_enabled_features,
update_tags,
select_known_packs,
custom_report_details,
server_links,
clear_dialog,
show_dialog,
]
);
}
pub struct ConfigPacketHandler<'a> {
@ -67,44 +67,45 @@ pub struct ConfigPacketHandler<'a> {
pub player: Entity,
}
impl ConfigPacketHandler<'_> {
pub fn registry_data(&mut self, p: ClientboundRegistryData) {
as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
pub fn registry_data(&mut self, p: &ClientboundRegistryData) {
as_system::<Query<&InstanceHolder>>(self.ecs, |mut query| {
let instance_holder = query.get_mut(self.player).unwrap();
let mut instance = instance_holder.instance.write();
// add the new registry data
instance.registries.append(p.registry_id, p.entries);
instance
.registries
.append(p.registry_id.clone(), p.entries.clone());
});
}
pub fn custom_payload(&mut self, p: ClientboundCustomPayload) {
pub fn custom_payload(&mut self, p: &ClientboundCustomPayload) {
debug!("Got custom payload packet {p:?}");
}
pub fn disconnect(&mut self, p: ClientboundDisconnect) {
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),
reason: Some(p.reason.clone()),
});
});
}
pub fn finish_configuration(&mut self, p: ClientboundFinishConfiguration) {
debug!("got FinishConfiguration packet: {p:?}");
pub fn finish_configuration(&mut self, _p: &ClientboundFinishConfiguration) {
debug!("got FinishConfiguration packet");
as_system::<(Commands, Query<&mut RawConnection>)>(
self.ecs,
|(mut commands, mut query)| {
let mut raw_conn = query.get_mut(self.player).unwrap();
raw_conn.state = ConnectionProtocol::Game;
raw_conn
.write_packet(ServerboundFinishConfiguration)
.expect(
"we should be in the right state and encoding this packet shouldn't fail",
);
raw_conn.set_state(ConnectionProtocol::Game);
commands.trigger(SendConfigPacketEvent::new(
self.player,
ServerboundFinishConfiguration,
));
// these components are added now that we're going to be in the Game state
commands
@ -120,38 +121,37 @@ impl ConfigPacketHandler<'_> {
);
}
pub fn keep_alive(&mut self, p: ClientboundKeepAlive) {
pub fn keep_alive(&mut self, p: &ClientboundKeepAlive) {
debug!(
"Got keep alive packet (in configuration) {p:?} for {:?}",
self.player
);
as_system::<(Query<&RawConnection>, EventWriter<_>)>(self.ecs, |(query, mut events)| {
let raw_conn = query.get(self.player).unwrap();
events.send(KeepAliveEvent {
as_system::<(Commands, EventWriter<_>)>(self.ecs, |(mut commands, mut events)| {
events.write(KeepAliveEvent {
entity: self.player,
id: p.id,
});
raw_conn
.write_packet(ServerboundKeepAlive { id: p.id })
.unwrap();
commands.trigger(SendConfigPacketEvent::new(
self.player,
ServerboundKeepAlive { id: p.id },
));
});
}
pub fn ping(&mut self, p: ClientboundPing) {
pub fn ping(&mut self, p: &ClientboundPing) {
debug!("Got ping packet (in configuration) {p:?}");
as_system::<Commands>(self.ecs, |mut commands| {
commands.trigger_targets(ConfigPingEvent(p), self.player);
commands.trigger_targets(ConfigPingEvent(p.clone()), self.player);
});
}
pub fn resource_pack_push(&mut self, p: ClientboundResourcePackPush) {
pub fn resource_pack_push(&mut self, p: &ClientboundResourcePackPush) {
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(),
@ -162,66 +162,71 @@ impl ConfigPacketHandler<'_> {
});
}
pub fn resource_pack_pop(&mut self, p: ClientboundResourcePackPop) {
pub fn resource_pack_pop(&mut self, p: &ClientboundResourcePackPop) {
debug!("Got resource pack pop packet {p:?}");
}
pub fn update_enabled_features(&mut self, p: ClientboundUpdateEnabledFeatures) {
pub fn update_enabled_features(&mut self, p: &ClientboundUpdateEnabledFeatures) {
debug!("Got update enabled features packet {p:?}");
}
pub fn update_tags(&mut self, _p: ClientboundUpdateTags) {
pub fn update_tags(&mut self, _p: &ClientboundUpdateTags) {
debug!("Got update tags packet");
}
pub fn cookie_request(&mut self, p: ClientboundCookieRequest) {
pub fn cookie_request(&mut self, p: &ClientboundCookieRequest) {
debug!("Got cookie request packet {p:?}");
as_system::<Query<&RawConnection>>(self.ecs, |query| {
let raw_conn = query.get(self.player).unwrap();
raw_conn
.write_packet(ServerboundCookieResponse {
key: p.key,
as_system::<Commands>(self.ecs, |mut commands| {
commands.trigger(SendConfigPacketEvent::new(
self.player,
ServerboundCookieResponse {
key: p.key.clone(),
// cookies aren't implemented
payload: None,
})
.unwrap();
},
));
});
}
pub fn reset_chat(&mut self, p: ClientboundResetChat) {
pub fn reset_chat(&mut self, p: &ClientboundResetChat) {
debug!("Got reset chat packet {p:?}");
}
pub fn store_cookie(&mut self, p: ClientboundStoreCookie) {
pub fn store_cookie(&mut self, p: &ClientboundStoreCookie) {
debug!("Got store cookie packet {p:?}");
}
pub fn transfer(&mut self, p: ClientboundTransfer) {
pub fn transfer(&mut self, p: &ClientboundTransfer) {
debug!("Got transfer packet {p:?}");
}
pub fn select_known_packs(&mut self, p: ClientboundSelectKnownPacks) {
pub fn select_known_packs(&mut self, p: &ClientboundSelectKnownPacks) {
debug!("Got select known packs packet {p:?}");
as_system::<Query<&RawConnection>>(self.ecs, |query| {
let raw_conn = query.get(self.player).unwrap();
as_system::<Commands>(self.ecs, |mut commands| {
// resource pack management isn't implemented
raw_conn
.write_packet(ServerboundSelectKnownPacks {
commands.trigger(SendConfigPacketEvent::new(
self.player,
ServerboundSelectKnownPacks {
known_packs: vec![],
})
.unwrap();
},
));
});
}
pub fn server_links(&mut self, p: ClientboundServerLinks) {
pub fn server_links(&mut self, p: &ClientboundServerLinks) {
debug!("Got server links packet {p:?}");
}
pub fn custom_report_details(&mut self, p: ClientboundCustomReportDetails) {
pub fn custom_report_details(&mut self, p: &ClientboundCustomReportDetails) {
debug!("Got custom report details packet {p:?}");
}
pub fn clear_dialog(&mut self, p: &ClientboundClearDialog) {
debug!("Got clear dialog packet {p:?}");
}
pub fn show_dialog(&mut self, p: &ClientboundShowDialog) {
debug!("Got show dialog packet {p:?}");
}
}

View file

@ -1,36 +1,27 @@
use std::{
io::Cursor,
sync::{Arc, Weak},
};
use std::sync::{Arc, Weak};
use azalea_chat::FormattedText;
use azalea_core::resource_location::ResourceLocation;
use azalea_protocol::{
packets::{
Packet,
game::{ClientboundGamePacket, ClientboundPlayerCombatKill, ServerboundGamePacket},
},
read::deserialize_packet,
use azalea_protocol::packets::{
Packet,
game::{ClientboundGamePacket, ClientboundPlayerCombatKill, ServerboundGamePacket},
};
use azalea_world::Instance;
use bevy_ecs::prelude::*;
use parking_lot::RwLock;
use tracing::{debug, error};
use tracing::{error, trace};
use uuid::Uuid;
use crate::{PlayerInfo, client::InGameState, raw_connection::RawConnection};
use crate::{client::InGameState, connection::RawConnection, player::PlayerInfo};
/// An event that's sent when we receive a packet.
/// ```
/// # use azalea_client::packet::game::ReceivePacketEvent;
/// # use azalea_client::packet::game::ReceiveGamePacketEvent;
/// # use azalea_protocol::packets::game::ClientboundGamePacket;
/// # use bevy_ecs::event::EventReader;
///
/// fn handle_packets(mut events: EventReader<ReceivePacketEvent>) {
/// for ReceivePacketEvent {
/// entity,
/// packet,
/// } in events.read() {
/// fn handle_packets(mut events: EventReader<ReceiveGamePacketEvent>) {
/// for ReceiveGamePacketEvent { entity, packet } in events.read() {
/// match packet.as_ref() {
/// ClientboundGamePacket::LevelParticles(p) => {
/// // ...
@ -41,7 +32,7 @@ use crate::{PlayerInfo, client::InGameState, raw_connection::RawConnection};
/// }
/// ```
#[derive(Event, Debug, Clone)]
pub struct ReceivePacketEvent {
pub struct ReceiveGamePacketEvent {
/// The client entity that received the packet.
pub entity: Entity,
/// The packet that was actually received.
@ -67,7 +58,7 @@ pub fn handle_outgoing_packets_observer(
) {
let event = trigger.event();
if let Ok((raw_connection, in_game_state)) = query.get_mut(event.sent_by) {
if let Ok((mut raw_connection, in_game_state)) = query.get_mut(event.sent_by) {
if in_game_state.is_none() {
error!(
"Tried to send a game packet {:?} while not in game state",
@ -76,10 +67,12 @@ pub fn handle_outgoing_packets_observer(
return;
}
// debug!("Sending packet: {:?}", event.packet);
if let Err(e) = raw_connection.write_packet(event.packet.clone()) {
trace!("Sending game packet: {:?}", event.packet);
if let Err(e) = raw_connection.write(event.packet.clone()) {
error!("Failed to send packet: {e}");
}
} else {
trace!("Not sending game packet: {:?}", event.packet);
}
}
@ -91,61 +84,6 @@ pub fn handle_outgoing_packets(mut commands: Commands, mut events: EventReader<S
}
}
pub fn emit_receive_packet_events(
query: Query<(Entity, &RawConnection), With<InGameState>>,
mut packet_events: ResMut<Events<ReceivePacketEvent>>,
) {
// we manually clear and send the events at the beginning of each update
// since otherwise it'd cause issues with events in process_packet_events
// running twice
packet_events.clear();
for (player_entity, raw_connection) in &query {
let packets_lock = raw_connection.incoming_packet_queue();
let mut packets = packets_lock.lock();
if !packets.is_empty() {
let mut packets_read = 0;
for raw_packet in packets.iter() {
packets_read += 1;
let packet =
match deserialize_packet::<ClientboundGamePacket>(&mut Cursor::new(raw_packet))
{
Ok(packet) => packet,
Err(err) => {
error!("failed to read packet: {err:?}");
debug!("packet bytes: {raw_packet:?}");
continue;
}
};
let should_interrupt = packet_interrupts(&packet);
packet_events.send(ReceivePacketEvent {
entity: player_entity,
packet: Arc::new(packet),
});
if should_interrupt {
break;
}
}
packets.drain(0..packets_read);
}
}
}
/// Whether the given packet should make us stop deserializing the received
/// packets until next update.
///
/// This is used for packets that can switch the client state.
fn packet_interrupts(packet: &ClientboundGamePacket) -> bool {
matches!(
packet,
ClientboundGamePacket::StartConfiguration(_)
| ClientboundGamePacket::Disconnect(_)
| ClientboundGamePacket::Transfer(_)
)
}
/// A player joined the game (or more specifically, was added to the tab
/// list of a local player).
#[derive(Event, Debug, Clone)]

View file

@ -1,202 +1,191 @@
mod events;
use std::{collections::HashSet, ops::Add, sync::Arc};
use std::{collections::HashSet, sync::Arc};
use azalea_core::{
game_type::GameMode,
math,
position::{ChunkPos, Vec3},
};
use azalea_entity::{
Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection,
Physics, Position, RelativeEntityUpdate,
Dead, EntityBundle, EntityKindComponent, LastSentPosition, LoadedBy, LocalEntity,
LookDirection, Physics, Position, RelativeEntityUpdate,
indexing::{EntityIdIndex, EntityUuidIndex},
metadata::{Health, apply_metadata},
};
use azalea_protocol::packets::game::*;
use azalea_protocol::{
common::movements::MoveFlags,
packets::{ConnectionProtocol, game::*},
};
use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
use bevy_ecs::{prelude::*, system::SystemState};
pub use events::*;
use tracing::{debug, error, trace, warn};
use crate::{
ClientInformation, PlayerInfo,
ClientInformation,
block_update::QueuedServerBlockUpdates,
chat::{ChatPacket, ChatReceivedEvent},
chunks, declare_packet_handlers,
chunks,
connection::RawConnection,
declare_packet_handlers,
disconnect::DisconnectEvent,
interact::BlockStatePredictionHandler,
inventory::{
ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent,
},
local_player::{
GameProfileComponent, Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList,
},
loading::HasClientLoaded,
local_player::{Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList},
movement::{KnockbackEvent, KnockbackType},
packet::as_system,
raw_connection::RawConnection,
player::{GameProfileComponent, PlayerInfo},
tick_counter::TicksConnected,
};
pub fn process_packet_events(ecs: &mut World) {
let mut events_owned = Vec::<(Entity, Arc<ClientboundGamePacket>)>::new();
pub fn process_packet(ecs: &mut World, player: Entity, packet: &ClientboundGamePacket) {
let mut handler = GamePacketHandler { player, ecs };
{
let mut system_state = SystemState::<EventReader<ReceivePacketEvent>>::new(ecs);
let mut events = system_state.get_mut(ecs);
for ReceivePacketEvent {
entity: player_entity,
packet,
} in events.read()
{
// we do this so `ecs` isn't borrowed for the whole loop
events_owned.push((*player_entity, packet.clone()));
}
}
for (player_entity, packet) in events_owned {
let mut handler = GamePacketHandler {
player: player_entity,
ecs,
};
// the order of these doesn't matter, that's decided by the protocol library
declare_packet_handlers!(
ClientboundGamePacket,
packet.as_ref(),
handler,
[
login,
set_chunk_cache_radius,
chunk_batch_start,
chunk_batch_finished,
custom_payload,
change_difficulty,
commands,
player_abilities,
set_cursor_item,
update_tags,
disconnect,
update_recipes,
entity_event,
player_position,
player_info_update,
player_info_remove,
set_chunk_cache_center,
chunks_biomes,
light_update,
level_chunk_with_light,
add_entity,
set_entity_data,
update_attributes,
set_entity_motion,
set_entity_link,
initialize_border,
set_time,
set_default_spawn_position,
set_health,
set_experience,
teleport_entity,
update_advancements,
rotate_head,
move_entity_pos,
move_entity_pos_rot,
move_entity_rot,
keep_alive,
remove_entities,
player_chat,
system_chat,
disguised_chat,
sound,
level_event,
block_update,
animate,
section_blocks_update,
game_event,
level_particles,
server_data,
set_equipment,
update_mob_effect,
award_stats,
block_changed_ack,
block_destruction,
block_entity_data,
block_event,
boss_event,
command_suggestions,
container_set_content,
container_set_data,
container_set_slot,
container_close,
cooldown,
custom_chat_completions,
delete_chat,
explode,
forget_level_chunk,
horse_screen_open,
map_item_data,
merchant_offers,
move_vehicle,
open_book,
open_screen,
open_sign_editor,
ping,
place_ghost_recipe,
player_combat_end,
player_combat_enter,
player_combat_kill,
player_look_at,
remove_mob_effect,
resource_pack_push,
resource_pack_pop,
respawn,
start_configuration,
entity_position_sync,
select_advancements_tab,
set_action_bar_text,
set_border_center,
set_border_lerp_size,
set_border_size,
set_border_warning_delay,
set_border_warning_distance,
set_camera,
set_display_objective,
set_objective,
set_passengers,
set_player_team,
set_score,
set_simulation_distance,
set_subtitle_text,
set_title_text,
set_titles_animation,
clear_titles,
sound_entity,
stop_sound,
tab_list,
tag_query,
take_item_entity,
bundle_delimiter,
damage_event,
hurt_animation,
ticking_state,
ticking_step,
reset_score,
cookie_request,
debug_sample,
pong_response,
store_cookie,
transfer,
move_minecart_along_track,
set_held_slot,
set_player_inventory,
projectile_power,
custom_report_details,
server_links,
player_rotation,
recipe_book_add,
recipe_book_remove,
recipe_book_settings,
test_instance_block_status,
]
);
}
// the order of these doesn't matter, that's decided by the protocol library
declare_packet_handlers!(
ClientboundGamePacket,
packet,
handler,
[
login,
set_chunk_cache_radius,
chunk_batch_start,
chunk_batch_finished,
custom_payload,
change_difficulty,
commands,
player_abilities,
set_cursor_item,
update_tags,
disconnect,
update_recipes,
entity_event,
player_position,
player_info_update,
player_info_remove,
set_chunk_cache_center,
chunks_biomes,
light_update,
level_chunk_with_light,
add_entity,
set_entity_data,
update_attributes,
set_entity_motion,
set_entity_link,
initialize_border,
set_time,
set_default_spawn_position,
set_health,
set_experience,
teleport_entity,
update_advancements,
rotate_head,
move_entity_pos,
move_entity_pos_rot,
move_entity_rot,
keep_alive,
remove_entities,
player_chat,
system_chat,
disguised_chat,
sound,
level_event,
block_update,
animate,
section_blocks_update,
game_event,
level_particles,
server_data,
set_equipment,
update_mob_effect,
award_stats,
block_changed_ack,
block_destruction,
block_entity_data,
block_event,
boss_event,
command_suggestions,
container_set_content,
container_set_data,
container_set_slot,
container_close,
cooldown,
custom_chat_completions,
delete_chat,
explode,
forget_level_chunk,
horse_screen_open,
map_item_data,
merchant_offers,
move_vehicle,
open_book,
open_screen,
open_sign_editor,
ping,
place_ghost_recipe,
player_combat_end,
player_combat_enter,
player_combat_kill,
player_look_at,
remove_mob_effect,
resource_pack_push,
resource_pack_pop,
respawn,
start_configuration,
entity_position_sync,
select_advancements_tab,
set_action_bar_text,
set_border_center,
set_border_lerp_size,
set_border_size,
set_border_warning_delay,
set_border_warning_distance,
set_camera,
set_display_objective,
set_objective,
set_passengers,
set_player_team,
set_score,
set_simulation_distance,
set_subtitle_text,
set_title_text,
set_titles_animation,
clear_titles,
sound_entity,
stop_sound,
tab_list,
tag_query,
take_item_entity,
bundle_delimiter,
damage_event,
hurt_animation,
ticking_state,
ticking_step,
reset_score,
cookie_request,
debug_sample,
pong_response,
store_cookie,
transfer,
move_minecart_along_track,
set_held_slot,
set_player_inventory,
projectile_power,
custom_report_details,
server_links,
player_rotation,
recipe_book_add,
recipe_book_remove,
recipe_book_settings,
test_instance_block_status,
waypoint,
clear_dialog,
show_dialog,
]
);
}
pub struct GamePacketHandler<'a> {
@ -246,7 +235,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)
@ -262,13 +251,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),
@ -311,6 +300,7 @@ impl GamePacketHandler<'_> {
previous: p.common.previous_game_type.into(),
},
entity_bundle,
TicksConnected(0),
));
azalea_entity::indexing::add_entity_to_indexes(
@ -335,15 +325,6 @@ impl GamePacketHandler<'_> {
.entity(self.player)
.insert(LoadedBy(HashSet::from_iter(vec![self.player])));
}
// send the client information that we have set
debug!(
"Sending client information because login: {:?}",
client_information
);
commands.trigger(SendPacketEvent::new(self.player,
azalea_protocol::packets::game::s_client_information::ServerboundClientInformation { information: client_information.clone() },
));
},
);
}
@ -358,7 +339,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,
});
});
@ -368,7 +349,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,
});
@ -409,7 +390,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()),
});
@ -444,68 +425,12 @@ impl GamePacketHandler<'_> {
**last_sent_position = **position;
fn apply_change<T: Add<Output = T>>(base: T, condition: bool, change: T) -> T {
if condition { base + change } else { change }
}
let new_x = apply_change(position.x, p.relative.x, p.change.pos.x);
let new_y = apply_change(position.y, p.relative.y, p.change.pos.y);
let new_z = apply_change(position.z, p.relative.z, p.change.pos.z);
let new_y_rot = apply_change(
direction.y_rot,
p.relative.y_rot,
p.change.look_direction.y_rot,
);
let new_x_rot = apply_change(
direction.x_rot,
p.relative.x_rot,
p.change.look_direction.x_rot,
);
let mut new_delta_from_rotations = physics.velocity;
if p.relative.rotate_delta {
let y_rot_delta = direction.y_rot - new_y_rot;
let x_rot_delta = direction.x_rot - new_x_rot;
new_delta_from_rotations = new_delta_from_rotations
.x_rot(math::to_radians(x_rot_delta as f64) as f32)
.y_rot(math::to_radians(y_rot_delta as f64) as f32);
}
let new_delta = Vec3::new(
apply_change(
new_delta_from_rotations.x,
p.relative.delta_x,
p.change.delta.x,
),
apply_change(
new_delta_from_rotations.y,
p.relative.delta_y,
p.change.delta.y,
),
apply_change(
new_delta_from_rotations.z,
p.relative.delta_z,
p.change.delta.z,
),
);
// apply the updates
physics.velocity = new_delta;
(direction.y_rot, direction.x_rot) = (new_y_rot, new_x_rot);
let new_pos = Vec3::new(new_x, new_y, new_z);
if new_pos != **position {
**position = new_pos;
}
p.relative
.apply(&p.change, &mut position, &mut direction, &mut physics);
// old_pos is set to the current position when we're teleported
physics.set_old_pos(&position);
physics.set_old_pos(*position);
// send the relevant packets
commands.trigger(SendPacketEvent::new(
self.player,
ServerboundAcceptTeleportation { id: p.id },
@ -513,10 +438,9 @@ impl GamePacketHandler<'_> {
commands.trigger(SendPacketEvent::new(
self.player,
ServerboundMovePlayerPosRot {
pos: new_pos,
look_direction: LookDirection::new(new_y_rot, new_x_rot),
// this is always false
on_ground: false,
pos: **position,
look_direction: *direction,
flags: MoveFlags::default(),
},
));
});
@ -551,9 +475,9 @@ 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: info.clone(),
info,
});
} else if let Some(info) = tab_list.get_mut(&updated_info.profile.uuid) {
// `else if` because the block for add_player above
@ -567,7 +491,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(),
});
@ -596,7 +520,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,
});
@ -610,7 +534,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();
@ -630,7 +554,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(),
});
@ -740,7 +664,7 @@ impl GamePacketHandler<'_> {
Query<(&EntityIdIndex, &InstanceHolder)>,
// this is a separate query since it's applied on the entity id that's being updated
// instead of the player that received the packet
Query<&EntityKind>,
Query<&EntityKindComponent>,
)>(self.ecs, |(mut commands, query, entity_kind_query)| {
let (entity_id_index, instance_holder) = query.get(self.player).unwrap();
@ -871,6 +795,8 @@ impl GamePacketHandler<'_> {
}
pub fn teleport_entity(&mut self, p: &ClientboundTeleportEntity) {
debug!("Got teleport entity packet {p:?}");
as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
self.ecs,
|(mut commands, mut query)| {
@ -881,26 +807,28 @@ impl GamePacketHandler<'_> {
return;
};
let new_pos = p.change.pos;
let new_look_direction = LookDirection {
x_rot: (p.change.look_direction.x_rot as i32 * 360) as f32 / 256.,
y_rot: (p.change.look_direction.y_rot as i32 * 360) as f32 / 256.,
};
let relative = p.relative.clone();
let change = p.change.clone();
commands.entity(entity).queue(RelativeEntityUpdate::new(
instance_holder.partial_instance.clone(),
move |entity| {
let mut position = entity.get_mut::<Position>().unwrap();
if new_pos != **position {
**position = new_pos;
}
let position = *position;
let mut look_direction = entity.get_mut::<LookDirection>().unwrap();
if new_look_direction != *look_direction {
*look_direction = new_look_direction;
}
// old_pos is set to the current position when we're teleported
let mut physics = entity.get_mut::<Physics>().unwrap();
physics.set_old_pos(&position);
let entity_id = entity.id();
entity.world_scope(move |world| {
let mut query =
world.query::<(&mut Physics, &mut LookDirection, &mut Position)>();
let (mut physics, mut look_direction, mut position) =
query.get_mut(world, entity_id).unwrap();
let old_position = *position;
relative.apply(
&change,
&mut position,
&mut look_direction,
&mut physics,
);
// old_pos is set to the current position when we're teleported
physics.set_old_pos(old_position);
});
},
));
},
@ -933,11 +861,7 @@ impl GamePacketHandler<'_> {
instance_holder.partial_instance.clone(),
move |entity_mut| {
let mut physics = entity_mut.get_mut::<Physics>().unwrap();
let new_pos = physics.vec_delta_codec.decode(
new_delta.xa as i64,
new_delta.ya as i64,
new_delta.za as i64,
);
let new_pos = physics.vec_delta_codec.decode(&new_delta);
physics.vec_delta_codec.set_base(new_pos);
physics.set_on_ground(new_on_ground);
@ -987,17 +911,13 @@ impl GamePacketHandler<'_> {
instance_holder.partial_instance.clone(),
move |entity_mut| {
let mut physics = entity_mut.get_mut::<Physics>().unwrap();
let new_pos = physics.vec_delta_codec.decode(
new_delta.xa as i64,
new_delta.ya as i64,
new_delta.za as i64,
);
physics.vec_delta_codec.set_base(new_pos);
let new_position = physics.vec_delta_codec.decode(&new_delta);
physics.vec_delta_codec.set_base(new_position);
physics.set_on_ground(new_on_ground);
let mut position = entity_mut.get_mut::<Position>().unwrap();
if new_pos != **position {
**position = new_pos;
if new_position != **position {
**position = new_position;
}
let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
@ -1051,7 +971,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,
});
@ -1103,7 +1023,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())),
});
@ -1114,7 +1034,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())),
});
@ -1125,7 +1045,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())),
});
@ -1141,12 +1061,9 @@ 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| {
let local_player = query.get_mut(self.player).unwrap();
let world = local_player.instance.write();
world.chunks.set_block_state(&p.pos, p.block_state);
as_system::<Query<&mut QueuedServerBlockUpdates>>(self.ecs, |mut query| {
let mut queued = query.get_mut(self.player).unwrap();
queued.list.push((p.pos, p.block_state));
});
}
@ -1157,13 +1074,11 @@ 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| {
let local_player = query.get_mut(self.player).unwrap();
let world = local_player.instance.write();
for state in &p.states {
world
.chunks
.set_block_state(&(p.section_pos + state.pos), state.state);
as_system::<Query<&mut QueuedServerBlockUpdates>>(self.ecs, |mut query| {
let mut queued = query.get_mut(self.player).unwrap();
for new_state in &p.states {
let pos = p.section_pos + new_state.pos;
queued.list.push((pos, new_state.state));
}
});
}
@ -1205,7 +1120,16 @@ impl GamePacketHandler<'_> {
pub fn award_stats(&mut self, _p: &ClientboundAwardStats) {}
pub fn block_changed_ack(&mut self, _p: &ClientboundBlockChangedAck) {}
pub fn block_changed_ack(&mut self, p: &ClientboundBlockChangedAck) {
as_system::<Query<(&InstanceHolder, &mut BlockStatePredictionHandler)>>(
self.ecs,
|mut query| {
let (local_player, mut prediction_handler) = query.get_mut(self.player).unwrap();
let world = local_player.instance.read();
prediction_handler.end_prediction_up_to(p.seq, &world);
},
);
}
pub fn block_destruction(&mut self, _p: &ClientboundBlockDestruction) {}
@ -1236,7 +1160,7 @@ impl GamePacketHandler<'_> {
}
}
} else {
events.send(SetContainerContentEvent {
events.write(SetContainerContentEvent {
entity: self.player,
slots: p.items.clone(),
container_id: p.container_id,
@ -1252,7 +1176,7 @@ impl GamePacketHandler<'_> {
// TODO: handle ContainerSetData packet
// this is used for various things like the furnace progress
// bar
// see https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol#Set_Container_Property
// see https://minecraft.wiki/w/Java_Edition_protocol/Packets#Set_Container_Property
// as_system::<Query<&mut Inventory>>(self.ecs, |mut query| {
// let inventory = query.get_mut(self.player).unwrap();
@ -1302,7 +1226,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,
});
});
@ -1319,7 +1243,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),
});
@ -1330,7 +1254,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();
@ -1353,7 +1277,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,
@ -1390,7 +1314,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()),
});
@ -1406,7 +1330,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(),
@ -1429,6 +1353,7 @@ impl GamePacketHandler<'_> {
&mut InstanceHolder,
&GameProfileComponent,
&ClientInformation,
Option<&mut InstanceName>,
),
With<LocalEntity>,
>,
@ -1438,11 +1363,23 @@ 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) =
query.get_mut(self.player).unwrap();
let Ok((mut instance_holder, game_profile, client_information, instance_name)) =
query.get_mut(self.player)
else {
warn!("Got respawn packet but player doesn't have the required components");
return;
};
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)
@ -1452,13 +1389,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),
@ -1497,8 +1434,9 @@ impl GamePacketHandler<'_> {
entity_bundle,
));
// Remove the Dead marker component from the player.
commands.entity(self.player).remove::<Dead>();
commands
.entity(self.player)
.remove::<(Dead, HasClientLoaded)>();
},
)
}
@ -1506,16 +1444,30 @@ impl GamePacketHandler<'_> {
pub fn start_configuration(&mut self, _p: &ClientboundStartConfiguration) {
debug!("Got start configuration packet");
as_system::<(Query<&RawConnection>, Commands)>(self.ecs, |(query, mut commands)| {
let raw_conn = query.get(self.player).unwrap();
let _ = raw_conn.write_packet(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) {
@ -1573,7 +1525,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) {}
@ -1597,7 +1551,16 @@ impl GamePacketHandler<'_> {
pub fn store_cookie(&mut self, _p: &ClientboundStoreCookie) {}
pub fn transfer(&mut self, _p: &ClientboundTransfer) {}
pub fn move_minecart_along_track(&mut self, _p: &ClientboundMoveMinecartAlongTrack) {}
pub fn set_held_slot(&mut self, _p: &ClientboundSetHeldSlot) {}
pub fn set_held_slot(&mut self, p: &ClientboundSetHeldSlot) {
debug!("Got set held slot packet {p:?}");
as_system::<Query<&mut Inventory>>(self.ecs, |mut query| {
let mut inventory = query.get_mut(self.player).unwrap();
if p.slot <= 8 {
inventory.selected_hotbar_slot = p.slot as u8;
}
});
}
pub fn set_player_inventory(&mut self, _p: &ClientboundSetPlayerInventory) {}
pub fn projectile_power(&mut self, _p: &ClientboundProjectilePower) {}
pub fn custom_report_details(&mut self, _p: &ClientboundCustomReportDetails) {}
@ -1607,4 +1570,12 @@ impl GamePacketHandler<'_> {
pub fn recipe_book_remove(&mut self, _p: &ClientboundRecipeBookRemove) {}
pub fn recipe_book_settings(&mut self, _p: &ClientboundRecipeBookSettings) {}
pub fn test_instance_block_status(&mut self, _p: &ClientboundTestInstanceBlockStatus) {}
pub fn waypoint(&mut self, _p: &ClientboundWaypoint) {}
pub fn clear_dialog(&mut self, p: &ClientboundClearDialog) {
debug!("Got clear dialog packet {p:?}");
}
pub fn show_dialog(&mut self, p: &ClientboundShowDialog) {
debug!("Got show dialog packet {p:?}");
}
}

View file

@ -1,114 +0,0 @@
// login packets aren't actually handled here because compression/encryption
// would make packet handling a lot messier
use std::{collections::HashSet, sync::Arc};
use azalea_protocol::packets::{
Packet,
login::{
ClientboundLoginPacket, ServerboundLoginPacket,
s_custom_query_answer::ServerboundCustomQueryAnswer,
},
};
use bevy_ecs::{prelude::*, system::SystemState};
use derive_more::{Deref, DerefMut};
use tokio::sync::mpsc;
use tracing::error;
// this struct is defined here anyways though so it's consistent with the other
// ones
/// An event that's sent when we receive a login packet from the server. Note
/// that if you want to handle this in a system, you must add
/// `.before(azalea::packet::login::process_packet_events)` to it
/// because that system clears the events.
#[derive(Event, Debug, Clone)]
pub struct LoginPacketEvent {
/// The client entity that received the packet.
pub entity: Entity,
/// The packet that was actually received.
pub packet: Arc<ClientboundLoginPacket>,
}
/// Event for sending a login packet to the server.
#[derive(Event)]
pub struct SendLoginPacketEvent {
pub entity: Entity,
pub packet: ServerboundLoginPacket,
}
impl SendLoginPacketEvent {
pub fn new(entity: Entity, packet: impl Packet<ServerboundLoginPacket>) -> Self {
let packet = packet.into_variant();
Self { entity, packet }
}
}
#[derive(Component)]
pub struct LoginSendPacketQueue {
pub tx: mpsc::UnboundedSender<ServerboundLoginPacket>,
}
/// A marker component for local players that are currently in the
/// `login` state.
#[derive(Component, Clone, Debug)]
pub struct InLoginState;
pub fn handle_send_packet_event(
mut send_packet_events: EventReader<SendLoginPacketEvent>,
mut query: Query<&mut LoginSendPacketQueue>,
) {
for event in send_packet_events.read() {
if let Ok(queue) = query.get_mut(event.entity) {
let _ = queue.tx.send(event.packet.clone());
} else {
error!("Sent SendPacketEvent for entity that doesn't have a LoginSendPacketQueue");
}
}
}
/// Plugins can add to this set if they want to handle a custom query packet
/// themselves. This component removed after the login state ends.
#[derive(Component, Default, Debug, Deref, DerefMut)]
pub struct IgnoreQueryIds(HashSet<u32>);
pub fn process_packet_events(ecs: &mut World) {
let mut events_owned = Vec::new();
let mut system_state: SystemState<ResMut<Events<LoginPacketEvent>>> = SystemState::new(ecs);
let mut events = system_state.get_mut(ecs);
for LoginPacketEvent {
entity: player_entity,
packet,
} in events.drain()
{
// we do this so `ecs` isn't borrowed for the whole loop
events_owned.push((player_entity, packet));
}
for (player_entity, packet) in events_owned {
#[allow(clippy::single_match)]
match packet.as_ref() {
ClientboundLoginPacket::CustomQuery(p) => {
let mut system_state: SystemState<(
EventWriter<SendLoginPacketEvent>,
Query<&IgnoreQueryIds>,
)> = SystemState::new(ecs);
let (mut send_packet_events, query) = system_state.get_mut(ecs);
let ignore_query_ids = query.get(player_entity).ok().map(|x| x.0.clone());
if let Some(ignore_query_ids) = ignore_query_ids {
if ignore_query_ids.contains(&p.transaction_id) {
continue;
}
}
send_packet_events.send(SendLoginPacketEvent::new(
player_entity,
ServerboundCustomQueryAnswer {
transaction_id: p.transaction_id,
data: None,
},
));
}
_ => {}
}
}
}

View file

@ -0,0 +1,86 @@
use std::sync::Arc;
use azalea_protocol::packets::{
Packet,
login::{
ClientboundCustomQuery, ClientboundHello, ClientboundLoginPacket, ServerboundLoginPacket,
},
};
use bevy_ecs::prelude::*;
use tracing::{debug, error};
use super::InLoginState;
use crate::{Account, connection::RawConnection};
#[derive(Event, Debug, Clone)]
pub struct ReceiveLoginPacketEvent {
/// The client entity that received the packet.
pub entity: Entity,
/// The packet that was actually received.
pub packet: Arc<ClientboundLoginPacket>,
}
#[derive(Event, Debug, Clone)]
pub struct ReceiveHelloEvent {
pub account: Account,
pub packet: ClientboundHello,
}
#[derive(Event, Debug, Clone)]
pub struct ReceiveCustomQueryEvent {
/// The client entity that received the packet.
pub entity: Entity,
pub packet: ClientboundCustomQuery,
/// A system can set this to `true` to make Azalea not reply to the query.
/// You must make sure you modify this before the
/// [`reply_to_custom_queries`] system runs.
///
/// [`reply_to_custom_queries`]: crate::login::reply_to_custom_queries
pub disabled: bool,
}
/// Event for sending a login packet to the server.
#[derive(Event, Debug, Clone)]
pub struct SendLoginPacketEvent {
pub sent_by: Entity,
pub packet: ServerboundLoginPacket,
}
impl SendLoginPacketEvent {
pub fn new(entity: Entity, packet: impl Packet<ServerboundLoginPacket>) -> Self {
let packet = packet.into_variant();
Self {
sent_by: entity,
packet,
}
}
}
pub fn handle_outgoing_packets_observer(
trigger: Trigger<SendLoginPacketEvent>,
mut query: Query<(&mut RawConnection, Option<&InLoginState>)>,
) {
let event = trigger.event();
if let Ok((mut raw_conn, in_login_state)) = query.get_mut(event.sent_by) {
if in_login_state.is_none() {
error!(
"Tried to send a login packet {:?} while not in login state",
event.packet
);
return;
}
debug!("Sending login packet: {:?}", event.packet);
if let Err(e) = raw_conn.write(event.packet.clone()) {
error!("Failed to send packet: {e}");
}
}
}
/// A system that converts [`SendLoginPacketEvent`] events into triggers so
/// they get received by [`handle_outgoing_packets_observer`].
pub fn handle_outgoing_packets(
mut commands: Commands,
mut events: EventReader<SendLoginPacketEvent>,
) {
for event in events.read() {
commands.trigger(event.clone());
}
}

View file

@ -0,0 +1,145 @@
// login packets aren't actually handled here because compression/encryption
// would make packet handling a lot messier
mod events;
use azalea_protocol::packets::{
ConnectionProtocol,
login::{
ClientboundCookieRequest, ClientboundCustomQuery, ClientboundHello,
ClientboundLoginCompression, ClientboundLoginDisconnect, ClientboundLoginFinished,
ClientboundLoginPacket, ServerboundCookieResponse, ServerboundLoginAcknowledged,
},
};
use bevy_ecs::prelude::*;
pub use events::*;
use tracing::{debug, error};
use super::as_system;
use crate::{
Account, InConfigState, connection::RawConnection, declare_packet_handlers,
disconnect::DisconnectEvent, player::GameProfileComponent,
};
pub fn process_packet(ecs: &mut World, player: Entity, packet: &ClientboundLoginPacket) {
let mut handler = LoginPacketHandler { player, ecs };
declare_packet_handlers!(
ClientboundLoginPacket,
packet,
handler,
[
hello,
login_disconnect,
login_finished,
login_compression,
custom_query,
cookie_request
]
);
}
/// A marker component for local players that are currently in the
/// `login` state.
#[derive(Component, Clone, Debug)]
pub struct InLoginState;
pub struct LoginPacketHandler<'a> {
pub ecs: &'a mut World,
pub player: Entity,
}
impl LoginPacketHandler<'_> {
pub fn hello(&mut self, p: &ClientboundHello) {
debug!("Got encryption request {p:?}");
as_system::<(Commands, Query<&Account>)>(self.ecs, |(mut commands, query)| {
let Ok(account) = query.get(self.player) else {
error!(
"Expected Account component to be present on player when receiving hello packet."
);
return;
};
commands.trigger_targets(
ReceiveHelloEvent {
account: account.clone(),
packet: p.clone(),
},
self.player,
);
});
}
pub fn login_disconnect(&mut self, p: &ClientboundLoginDisconnect) {
debug!("Got disconnect {:?}", p);
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.write(DisconnectEvent {
entity: self.player,
reason: Some(p.reason.clone()),
});
});
}
pub fn login_finished(&mut self, p: &ClientboundLoginFinished) {
debug!(
"Got profile {:?}. login is finished and we're now switching to the config state",
p.game_profile
);
as_system::<(Commands, Query<&mut RawConnection>)>(
self.ecs,
|(mut commands, mut query)| {
commands.trigger(SendLoginPacketEvent::new(
self.player,
ServerboundLoginAcknowledged,
));
commands
.entity(self.player)
.remove::<InLoginState>()
.insert(InConfigState)
.insert(GameProfileComponent(p.game_profile.clone()));
let mut conn = query
.get_mut(self.player)
.expect("RawConnection component should be present when receiving packets");
conn.state = ConnectionProtocol::Configuration;
},
);
}
pub fn login_compression(&mut self, p: &ClientboundLoginCompression) {
debug!("Got compression request {p:?}");
as_system::<Query<&mut RawConnection>>(self.ecs, |mut query| {
let mut conn = query
.get_mut(self.player)
.expect("RawConnection component should be present when receiving packets");
if let Some(net_conn) = &mut conn.net_conn() {
net_conn.set_compression_threshold(Some(p.compression_threshold as u32));
}
})
}
pub fn custom_query(&mut self, p: &ClientboundCustomQuery) {
debug!("Got custom query {p:?}");
as_system::<EventWriter<ReceiveCustomQueryEvent>>(self.ecs, |mut events| {
events.write(ReceiveCustomQueryEvent {
entity: self.player,
packet: p.clone(),
disabled: false,
});
});
}
pub fn cookie_request(&mut self, p: &ClientboundCookieRequest) {
debug!("Got cookie request {p:?}");
as_system::<Commands>(self.ecs, |mut commands| {
commands.trigger(SendLoginPacketEvent::new(
self.player,
ServerboundCookieResponse {
key: p.key.clone(),
// cookies aren't implemented
payload: None,
},
));
});
}
}

View file

@ -1,17 +1,11 @@
use azalea_entity::metadata::Health;
use bevy_app::{App, First, Plugin, PreUpdate, Update};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::{
prelude::*,
system::{SystemParam, SystemState},
};
use self::{
game::{
AddPlayerEvent, DeathEvent, InstanceLoadedEvent, KeepAliveEvent, RemovePlayerEvent,
ResourcePackEvent, UpdatePlayerEvent,
},
login::{LoginPacketEvent, SendLoginPacketEvent},
};
use self::game::DeathEvent;
use crate::{chat::ChatReceivedEvent, events::death_listener};
pub mod config;
@ -26,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,
});
@ -36,50 +30,38 @@ pub fn death_event_on_0_health(
impl Plugin for PacketPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
First,
(
game::emit_receive_packet_events,
config::emit_receive_config_packet_events,
),
)
.add_systems(
PreUpdate,
(
game::process_packet_events,
config::process_packet_events,
login::handle_send_packet_event,
login::process_packet_events,
),
)
.add_observer(game::handle_outgoing_packets_observer)
.add_observer(config::handle_outgoing_packets_observer)
.add_systems(
Update,
(
app.add_observer(game::handle_outgoing_packets_observer)
.add_observer(config::handle_outgoing_packets_observer)
.add_observer(login::handle_outgoing_packets_observer)
.add_systems(
Update,
(
config::handle_outgoing_packets,
game::handle_outgoing_packets,
)
.chain(),
death_event_on_0_health.before(death_listener),
),
)
// we do this instead of add_event so we can handle the events ourselves
.init_resource::<Events<game::ReceivePacketEvent>>()
.init_resource::<Events<config::ReceiveConfigPacketEvent>>()
.add_event::<game::SendPacketEvent>()
.add_event::<config::SendConfigPacketEvent>()
.add_event::<AddPlayerEvent>()
.add_event::<RemovePlayerEvent>()
.add_event::<UpdatePlayerEvent>()
.add_event::<ChatReceivedEvent>()
.add_event::<DeathEvent>()
.add_event::<KeepAliveEvent>()
.add_event::<ResourcePackEvent>()
.add_event::<InstanceLoadedEvent>()
.add_event::<LoginPacketEvent>()
.add_event::<SendLoginPacketEvent>();
(
config::handle_outgoing_packets,
game::handle_outgoing_packets,
login::handle_outgoing_packets,
)
.chain(),
death_event_on_0_health.before(death_listener),
),
)
.add_event::<game::ReceiveGamePacketEvent>()
.add_event::<config::ReceiveConfigPacketEvent>()
.add_event::<login::ReceiveLoginPacketEvent>()
//
.add_event::<game::SendPacketEvent>()
.add_event::<config::SendConfigPacketEvent>()
.add_event::<login::SendLoginPacketEvent>()
//
.add_event::<game::AddPlayerEvent>()
.add_event::<game::RemovePlayerEvent>()
.add_event::<game::UpdatePlayerEvent>()
.add_event::<ChatReceivedEvent>()
.add_event::<game::DeathEvent>()
.add_event::<game::KeepAliveEvent>()
.add_event::<game::ResourcePackEvent>()
.add_event::<game::InstanceLoadedEvent>()
.add_event::<login::ReceiveCustomQueryEvent>();
}
}

View file

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

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