1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 23:44:38 +00:00

Compare commits

..

398 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
mat
e2945b90a9 Release 0.12.0
azalea@0.12.0
azalea-auth@0.12.0
azalea-block@0.12.0
azalea-block-macros@0.12.0
azalea-brigadier@0.12.0
azalea-buf@0.12.0
azalea-buf-macros@0.12.0
azalea-chat@0.12.0
azalea-client@0.12.0
azalea-core@0.12.0
azalea-crypto@0.12.0
azalea-entity@0.12.0
azalea-inventory@0.12.0
azalea-inventory-macros@0.12.0
azalea-language@0.12.0
azalea-physics@0.12.0
azalea-protocol@0.12.0
azalea-protocol-macros@0.12.0
azalea-registry@0.12.0
azalea-registry-macros@0.12.0
azalea-world@0.12.0

Generated by cargo-workspaces
2025-04-12 21:08:44 -05:00
mat
ed4d5937a7 upgrade deps 2025-04-12 21:29:36 -04:30
mat
31b143f821 fix ChickenVariant data component 2025-04-12 13:37:27 -12:00
mat
3112dc3ea0 fix incorrect data components 2025-04-13 02:38:29 +06:00
mat
1be8d8638f optimization: only run update_on_climbable for local entities 2025-04-13 01:45:07 +06:00
mat
913d6ac8c5 clippy 2025-04-04 09:23:28 -13:45
mat
7dcc86e266 remove unwrap in Hello packet handler 2025-04-05 06:37:36 +08:00
mat
adf8a604c4 fix ordering for handle_attack_queued 2025-04-04 13:48:48 -08:45
mat
efc28db6cf remove unnecessary ecs system ordering for handle_outgoing_packets 2025-04-05 01:46:14 +03:30
mat
f250978cdd send attack packets at the end of the tick 2025-04-05 00:50:39 +03:30
mat
5fd57fd630 don't require mut for functions in Client and add some more convenience functions 2025-04-04 16:05:06 -04:30
mat
e99ae608b6 rename ChatPacket::username and uuid to sender and sender_uuid
also adds more convenience functions to Client
2025-04-04 18:24:22 -02:00
mat
1fd0292590 rename ChatPacket::username and uuid to sender and sender_uuid 2025-04-04 06:23:19 -14:00
mat
09d515c8cd breaking change: send azalea::Event::Spawn every time we switch worlds instead of only once ever 2025-04-04 18:09:19 +00:00
mat
5272db8bb4 remove executable bit from files 2025-04-04 15:56:06 +00:00
mat
635bed9fcb fix compilation related to carried_item 2025-03-29 23:39:02 +00:00
mat
097ebb3705 use HashedStack for ServerboundContainerClick::carried_item too 2025-03-29 23:20:26 +00:00
mat
55ed46d081 fix incorrect usage of optional dependency 2025-03-29 18:15:49 -05:00
mat
43ebbee94a update ServerboundContainerClick to use HashedStack from 1.21.5 2025-03-29 23:10:55 +00:00
mat
d0b459e827 add support for unnamed structs to azalea-buf-macros 2025-03-29 22:29:37 +00:00
mat
4e5c551d65 fix entity deindexing happening at the wrong time 2025-03-28 00:54:17 +00:00
mat
02de98240f fix not replying to ping packets in config state 2025-03-27 21:28:13 +00:00
mat
37cde3ad32 fix components, show_in_tooltip was removed 2025-03-26 00:00:25 +00:00
mat
ef357fdf36
1.21.5 (#198)
* 25w02a

* move item_components codegen to a different module

* remove outdated test

* 25w03a

* start updating to 24w09b

* 1.21.5-pre2

* fix broken packets

* 1.21.5-rc2

* merge main

* delete unused acket_handling

* 1.21.5
2025-03-25 11:17:39 -05:00
mat
8af265e48b PongPlugin 2025-03-25 05:16:10 +00:00
mat
4c53498f07 ignore clippy warning on spawn_listener 2025-03-25 04:32:03 +00:00
EightFactorial
b25474ad66
Wait for plugins to load before starting the ECS (#212)
* Wait for plugins to load before starting the ECS

Also runs `App:finish()` after plugins finish loading.

* Forgot to run `App::cleanup()`

Oops haha

* copy a check from bevy and don't busywait

---------

Co-authored-by: mat <git@matdoes.dev>
2025-03-24 23:30:33 -05:00
mat
656162d218 make events channel in Client::start_client optional 2025-03-25 04:06:47 +00:00
mat
113222a3a2 use zlib-rs for compression 2025-03-25 03:54:22 +00:00
mat
aa2039c868 upgrade deps and slightly improve docs 2025-03-20 03:35:59 +00:00
mat
5c7332b469 add Event::Spawn 2025-03-20 03:30:57 +00:00
mat
13d5cbed1f document Position default 2025-03-20 01:49:15 +00:00
mat
75efbc83fd make SendPacketEvent a bevy trigger 2025-03-19 21:35:47 +00:00
mat
ca2e0b3922 entity collisions 2025-03-16 19:01:31 +00:00
mat
b0bd992adc
Fluid physics fixes (#210)
* start fixing code related to fluid physics

* implement force_solid for blocks

* afk pool test
2025-03-16 13:41:17 -05:00
mat
a95408cbcc fix broken doc links 2025-03-16 04:36:42 +00:00
mat
5f5616145b document ClientboundAddEntity::data 2025-03-16 04:34:32 +00:00
mat
d6a4d91347 impl from u32 for minecraftentityid 2025-03-16 04:23:14 +00:00
Gjum
aeff08f5fa
update dead wiki.vg links (#209) 2025-03-13 17:03:42 -05:00
mat
7981230904 mention NO_COLOR=1 in section about logging in docs 2025-03-13 21:34:54 +00:00
mat
95bdb362ba improve Swarm docs and change an indexing warning to a debug log 2025-03-13 21:27:48 +00:00
mat
f8140e4188 use RawConnection to write reply after getting start_configuration 2025-03-13 21:21:59 +00:00
mat
713b312167 correctly accept resource packs while in config state 2025-03-13 21:01:37 +00:00
mat
249fa55a53 fix wrong Potion data component 2025-03-13 20:52:29 +00:00
mat
65fe072151 fix wrong Consumable component implementation and add set_equipment test 2025-03-13 20:46:12 +00:00
mat
7a192acc99 properly remove from the EntityIdIndex component on despawn 2025-03-13 20:13:58 +00:00
mat
e9a5df2e87 remove EntityBundle on start_configuration 2025-03-07 19:17:23 +00:00
mat
42c79043cf don't send game packets when outside of the game state 2025-03-07 19:11:24 +00:00
mat
28a17f3ed2 add bots and plugins to readme 2025-03-06 04:33:30 +00:00
mat
cf66c4be10 fix despawning entities on dimension change 2025-03-06 04:11:19 +00:00
mat
c9022e8f67 fix errors when switching to Game state and add fast_login test 2025-03-02 05:42:15 +00:00
mat
72d87349fe update outdated comment 2025-02-24 04:26:16 +00:00
mat
7d6cacc79b fix send_receivepacketevent running outside of game state 2025-02-24 04:24:52 +00:00
mat
cfe6bea25a fix compile error 2025-02-24 04:15:33 +00:00
mat
9e061be903 fix an error in set_entity_data 2025-02-24 04:14:54 +00:00
mat
172e0ce079 add fallback for adding LocalEntity so tests don't fail 2025-02-24 03:59:57 +00:00
mat
b9767424f3 don't remove LocalEntity from disconnected players, add new debug logs, and make GameProfile clones cheaper 2025-02-24 03:55:29 +00:00
mat
6a5ab34a2d azalea-language now does a binary search instead of a hashmap lookup 2025-02-23 17:39:17 +00:00
mat
2be4f0f2b6 make run_schedule a bounded channel 2025-02-23 17:10:47 +00:00
mat
21acf4c846 try to receive packets all at once before sending run_schedule_sender 2025-02-23 09:05:20 +00:00
mat
dd557c8f29 fix memory leak in simulation tests (lol)
also, change some vecs into boxed slices, and add RelativeEntityUpdate::new
2025-02-23 08:47:17 +00:00
mat
e21e1b97bf
Refactor azalea-client (#205)
* start organizing packet_handling more by moving packet handlers into their own functions

* finish writing all the handler functions for packets

* use macro for generating match statement for packet handler functions

* fix set_entity_data

* update config state to also use handler functions

* organize az-client file structure by moving things into plugins directory

* fix merge issues
2025-02-22 21:45:26 -06:00
mat
f8130c3c92 minor memory usage optimizations 2025-02-23 03:10:21 +00:00
mat
34f53baf85 update to rust edition 2024 2025-02-22 23:01:54 +00:00
mat
bdd2fc91e1 update deps 2025-02-22 22:56:07 +00:00
mat
6fdf5fce49 fix brand and client info so they're only sent when leaving login instead of entering config
closes #206
2025-02-22 22:34:06 +00:00
mat
444993b609 debug log CARGO_PKG_VERSION on swarm start 2025-02-22 21:52:23 +00:00
mat
74b52a1fc1 fix is_trapdoor_useable_as_ladder and add test 2025-02-22 21:09:35 +00:00
mat
27945c8870 despawn entities when switching worlds and some testbot fixes 2025-02-21 22:50:39 +00:00
mat
f5f15362f2 fix some components not being removed from clients and add debugecsleak testbot command 2025-02-21 21:51:26 +00:00
mat
5fdea4c0b7 fix wrong docs for testbot 2025-02-21 20:20:17 +00:00
mat
24babbdbe5 GameTick should only happen after Update 2025-02-21 20:17:47 +00:00
mat
833f306e8b
Fix errors on switching dimensions (#204)
* Fix errors on switching dimensions

* fix other tests

* clippy

* fix log feature in test_simulation

* fix chunks oops
2025-02-21 13:43:56 -06:00
mat
63b1036a96 fix CustomModelData and WrittenBookContent datacomponents 2025-02-21 17:22:00 +00:00
Shayne Hartford
c285fadd34
Enable serde feature in sub-crates (#202)
Add serde derive to MinecraftEntityId
2025-02-17 17:28:37 -06:00
mat
022771b71b update docs for TabList to mention how it works as a resource 2025-02-17 23:09:24 +00:00
mat
228489dded use MinecraftEntityId type instead of u32 in az-protocol 2025-02-16 17:10:04 +00:00
mat
4fb6b07746 upgrade deps 2025-02-14 03:56:36 +00:00
mat
02f50c2dc5 remove stabilized trait_upcasting feature gates 2025-02-14 03:40:51 +00:00
mat
6020a2297f add todo about wrong calculated_bits_per_entry code 2025-02-04 05:29:18 +00:00
Kumpelinus
858bec2081
Fix deadlock when changing server (#200)
* Fix deadlock when changing server

* remove unnecessary clones

---------

Co-authored-by: mat <git@matdoes.dev>
2025-02-03 13:31:32 -06:00
mat
8d110a9f7c cleanup, fix warnings 2025-02-02 21:17:28 +00:00
mat
b08d3d55d7 start implementing data driven registries 2025-02-02 21:15:45 +00:00
mat
cdb68dfb70 update deps 2025-02-02 17:53:04 +00:00
mat
479a7b8927 update actions/upload-pages-artifact and deploy-pages 2025-02-01 21:37:07 +00:00
mat
8d71577da8 fix doc errors 2025-02-01 21:35:11 +00:00
mat
4562967b61 set player metadata on init instead of login 2025-02-01 21:26:37 +00:00
mat
87c34e1c33 add failing test_set_health_before_login test 2025-02-01 20:35:57 +00:00
mat
4aa5010ea2 remove debug log 2025-01-29 06:07:16 +00:00
mat
5bdb07b314 fix PlayerInput to handle diagonal inputs 2025-01-29 06:04:38 +00:00
mat
6c681adc4d change some warns to debugs 2025-01-29 05:45:17 +00:00
mat
6d8d937d47 add support for tickend and playerinput 2025-01-29 05:29:20 +00:00
mat
befcec9b3a fix wrong Profile data component impl 2025-01-29 02:09:54 +00:00
mat
ef25e77e52 add data for EntityEffect particle to fix set_entity_data errors on hypixel 2025-01-29 02:02:39 +00:00
mat
67c053638c fix chunk parsing on hypixel 2025-01-27 19:24:12 +00:00
mat
ed00a5b8f1 don't warn on release mode for unknown PlayerInfoUpdate 2025-01-27 18:14:53 +00:00
mat
fe423416b4 Rename Connection::configuration to config and add some clientbound functions that already existed serverbound
taken from Shay's fork: b0ca6076ed
2025-01-25 22:34:00 +00:00
mat
b6ddde99ea add ProtocolPacket::name function 2025-01-25 22:20:35 +00:00
mat
f2d8d4211b fix name of field in CommonPlayerSpawnInfo 2025-01-21 23:55:44 +00:00
mat
b0b57aa7d4 delete accidentally created debug file 2025-01-21 23:54:11 +00:00
mat
53fca5faf4 fix errors when switching worlds 2025-01-21 23:53:11 +00:00
mat
900a4234e5 fix wrong ClientboundSoundEntity 2025-01-21 23:21:08 +00:00
mat
aef29daad9 remove negative checks for box_shape 2025-01-20 05:49:13 +00:00
mat
2dcfbe96c3 fix wrong ServerLinkKind and serialize hex colors correctly in nbt 2025-01-14 03:18:38 +00:00
mat
a86d011d4a change some warnings to debug logs and improve some entity id index code 2025-01-13 06:11:14 +00:00
mat
5721eaf193 fix ClientboundSound and implement az_registry::Holder 2025-01-13 04:52:40 +00:00
mat
862dec529b update warning in readme to say azalea is 'useable' instead of 'somewhat useable' 2025-01-12 23:01:55 +00:00
mat
a1435b3b95 fix bugs with decoding/encoding DataComponentPatch 2025-01-12 22:43:37 +00:00
mat
093c99a071 add AZALEA_DO_NOT_CUT_OFF_PACKET_LOGS env variable 2025-01-12 21:51:37 +00:00
mat
608ccb8e54 fix panic on bot disconnect 2025-01-12 05:07:53 +00:00
mat
a5cb21f039 codegen entity dimensions 2025-01-11 08:14:19 +00:00
mat
e03101a7b7 dead entities cannot be revived 2025-01-11 04:35:10 +00:00
mat
31d0ac7568 update wrong entity metadata 2025-01-11 04:09:39 +00:00
mat
55d1ae2014 revert doc updates 2025-01-10 23:54:33 +00:00
mat
9e98ab373f
Create CNAME 2025-01-10 17:34:34 -06:00
mat
3ce7c7cc50 tweaks to doc gh action 2025-01-10 23:08:51 +00:00
Aditya Kumar
9ef53fcf2b
Feat: Host Documentation for multiple versions/branch (#188)
* Better Docs

* Made action only to run on `main` or version branch

* Moved README.md to BRANCHES.md

* Redirect to main branch by default

* Moved Branches.md to branches.html
2025-01-10 17:08:23 -06:00
mat
0d16f01571
Fluid physics (#199)
* start implementing fluid physics

* Initial implementation of fluid pushing

* different travel function in water

* bubble columns

* jumping in water

* cleanup

* change ultrawarm to be required

* fix for clippy
2025-01-10 16:45:27 -06:00
mat
615d8f9d2a bump minimum rust version and improve pathfinder docs 2024-12-28 02:10:05 +00:00
mat
ebaf5128fb better pathfinder debug messages 2024-12-28 01:48:25 +00:00
mat
5693191b57 implement fluid_shape 2024-12-27 12:35:25 +00:00
mat
04036b6e4a implement BlockState::outline_shape 2024-12-27 12:35:18 +00:00
mat
6ccd44e28d better astar WeightedNode::cmp 2024-12-27 06:57:08 +00:00
mat
185ed84dbb don't save Movement type while pathfinding as an optimization, recalculate it later in reconstruct_path 2024-12-27 06:27:16 +00:00
mat
3c3952bb0b resolve some todos in az-core 2024-12-27 05:43:35 +00:00
mat
33e1a1326a patch path on timeout instead of recalculating everything 2024-12-26 12:36:41 +00:00
mat
344834c724 better pathfinder timeouts 2024-12-26 08:03:55 +00:00
mat
adb56b7eb2 make a_star function use an IndexMap like the pathfinding crate 2024-12-26 07:42:35 +00:00
mat
3c83e5b24a replace priority_queue crate with std BinaryHeap 2024-12-26 05:52:46 +00:00
mat
e11a902fba patch pathfinder obstructions instead of just truncating the path 2024-12-25 12:09:27 +00:00
mat
2f1fe5f9f6 partial fix for sending Config packets during Game 2024-12-25 09:58:32 +00:00
mat
8f0d0d9280 close tcp connection on bot disconnect and add swarms to testbot cli 2024-12-25 07:27:09 +00:00
mat
04eaa5c3d0 remove dependency on bytes crate for azalea-protocol and fix memory leak 2024-12-25 06:16:10 +00:00
mat
0ee9ed50e3 optimize pathfinder 2024-12-25 03:00:48 +00:00
mat
d67aa07c13 optimize cost_for_breaking_block by making its cache an UnsafeCell instead 2024-12-24 12:41:18 +00:00
mat
a599b5614e make BlockState a u16 and add a BlockStateIntegerRepr type 2024-12-24 09:40:29 +00:00
mat
f03e0c2235 fix parsing Dust particle and treat waterlogged blocks as liquid in pathfinder 2024-12-24 08:48:36 +00:00
mat
de5a53ce08 add additional pathfinder test and fix pathfinder failure 2024-12-24 05:18:33 +00:00
mat
958848e8ed improve some docs and apis related to pathfinder 2024-12-24 04:37:55 +00:00
mat
30cbeecdfe update outdated doc in Swarm and remove unnecessary generics 2024-12-23 13:10:40 +00:00
mat
8772661772 lift requirement on anyhow for handler function 2024-12-23 10:34:44 +00:00
mat
1609b90a93 rename metadata::State to metadata::SnifferState 2024-12-23 09:55:21 +00:00
mat
fa132b61fc add a bit more logging to handshake_proxy 2024-12-23 07:31:51 +00:00
mat
2c37ade959 add a test for 2b2t queue chat message 2024-12-23 02:01:58 +00:00
mat
63fef9d94f fix 'extra' parsing in FormattedText::from_nbt_compound which results in duplicated chat fragments 2024-12-23 02:01:09 +00:00
mat
a477a84c6e exit get_block_state early if below the world 2024-12-23 01:31:17 +00:00
mat
3b1fe490b0 make testbot take cli args 2024-12-23 01:12:27 +00:00
mat
478fe722f5 fix unlinked link in comment 2024-12-21 03:09:40 +00:00
mat
641b99c7af update trust-dns -> hickory-dns 2024-12-21 03:06:56 +00:00
mat
98e5efb577 fix flakey pathfinder tests by moving stop_pathfinding_on_instance_change to be before path_found_listener 2024-12-21 02:57:19 +00:00
mat
831e8c167f fix compiling azalea-registry 2024-12-21 02:40:53 +00:00
mat
ad30950f85 Release independent packages
Generated by cargo-workspaces
2024-12-20 19:22:29 -06:00
mat
7098375ecf Release 0.11.0+mc1.21.4
azalea@0.11.0+mc1.21.4
azalea-auth@0.11.0+mc1.21.4
azalea-block@0.11.0+mc1.21.4
azalea-block-macros@0.11.0+mc1.21.4
azalea-brigadier@0.11.0+mc1.21.4
azalea-buf@0.11.0+mc1.21.4
azalea-buf-macros@0.11.0+mc1.21.4
azalea-chat@0.11.0+mc1.21.4
azalea-client@0.11.0+mc1.21.4
azalea-core@0.11.0+mc1.21.4
azalea-crypto@0.11.0+mc1.21.4
azalea-entity@0.11.0+mc1.21.4
azalea-inventory@0.11.0+mc1.21.4
azalea-inventory-macros@0.11.0+mc1.21.4
azalea-language@0.11.0+mc1.21.4
azalea-physics@0.11.0+mc1.21.4
azalea-protocol@0.11.0+mc1.21.4
azalea-protocol-macros@0.11.0+mc1.21.4
azalea-registry@0.11.0+mc1.21.4
azalea-registry-macros@0.11.0+mc1.21.4
azalea-world@0.11.0+mc1.21.4

Generated by cargo-workspaces
2024-12-20 19:18:36 -06:00
mat
527333f2b7 more verbose login_disconnect error 2024-12-21 00:02:54 +00:00
mat
78e5a65317 fix incorrect tags codegen 2024-12-20 23:59:17 +00:00
mat
c7cb73d22b update entity metadata 2024-12-20 23:56:43 +00:00
mat
5fbad9bbb3 update deps 2024-12-20 23:54:04 +00:00
mat
e268c49291 fix incorrect packets 2024-12-19 02:52:41 +00:00
Gaspard Culis
1f06a1540f
chore(core): Derive Serialize and Deserialize for direction types (#197)
When crate's `serde` feature is enabled
2024-12-12 15:13:16 -06:00
mat
e9136c9cbb
Implement EntityPositionSync (#196)
* implement EntityPositionSync

* fix EntityPositionSync setting the wrong vec_delta_codec and also move into a RelativeEntityUpdate
2024-12-11 19:51:12 -06:00
mat
23932003d9 remove the generic_const_exprs feature since it's unused and to make the rust compiler devs happy 2024-12-11 05:39:23 +00:00
mat
f4a1869a7d fix incorrect EntityDataValue::Long 2024-12-11 03:54:12 +00:00
mat
097a620de1 fix for latest nightly by changing the FixedBitSet generic to take bytes instead of bits 2024-12-11 03:25:36 +00:00
EightFactorial
2feef49471
Disable the deadlock_detection feature by default (#195)
* Disable the `deadlock_detection` feature by default

Fixes conflicts with any packages that enable parking_lot's `send_guard` feature

* move testbot deadlock detection to a function and add additional comments

---------

Co-authored-by: mat <git@matdoes.dev>
2024-12-10 16:37:35 -06:00
EightFactorial
07109964ad
Emit a build warning if the compiler may fail to build (#194)
This should be reverted when the latest nightly can build again
2024-12-07 17:23:27 -06:00
mat
39f4d68e1f fix container_set_content, player_position, and recipe_book_remove packets 2024-12-05 06:30:47 +00:00
EightFactorial
6379035b85
Update Bevy and migrate to workspace dependencies and package attributes (#181)
* Use workspace `Cargo.toml` for dependencies and package atributes

* Fix a couple clippy warnings

* Update bevy, update build script, move deps to workspace, and fix clippy warnings

* Remove carrots from crate versions
The default behavior is the same

* Remove unused dependencies
Compiles and all tests pass, so it should be fine

* Update codegen to use `std::sync::LazyLock` instead of `once_cell::sync::Lazy`

* Update Bevy to `0.15.0-rc.3`
Surprisingly little needed to be changed

* Update to bevy 0.15.0

* Fix leftover merge issues

* Clarify the reason the swarm can't connect

* Fix duplicate lint, remove `log` dependency
2024-12-04 18:31:22 -06:00
mat
241c7527ce add link to 1.21.3 branch in readme 2024-12-03 20:46:36 +00:00
mat
b71a7af53a
1.21.4 (#189)
* start updating to 1.21.4

* fix block codegen and stop using block data from burger

* 1.21.4-rc1

* 1.21.4
2024-12-03 14:42:23 -06:00
Shayne Hartford
ea5a1c1ec1
Add missing world border boolean to use item on packet in 1.21.2 (#192) 2024-11-30 18:05:35 -06:00
Shayne Hartford
944fe5c6f8
Impl FromStr for BlockPos & Vec (again) (#191) 2024-11-30 15:46:39 -06:00
mat
f364ad6b21 switch some packet structs to unit structs 2024-11-28 20:33:39 +00:00
mat
ae5c0ea8e5 improve DataComponentPatch::has and has_kind 2024-11-28 03:22:34 +00:00
mat
c36201cc89 change DataComponentPatch::get to take in a generic (and add get_kind, has, and has_kind) 2024-11-28 03:17:58 +00:00
mat
08958c2278
Refactor azalea-protocol (#190)
* start updating to 1.21.4

* fix block codegen and stop using block data from burger

* rename packet related modules and structs to be simpler

* ItemSlot -> ItemStack for more consistency with mojmap

* .get() -> .into_packet()

* simplify declare_state_packets by removing packet ids

* rename read_from and write_into to azalea_read and azalea_write

* rename McBufReadable and McBufWritable to AzaleaRead and AzaleaWrite

* McBuf -> AzBuf

* remove most uses of into_variant

* update codegen and use resourcelocation names for packets

* implement #[limit(i)] attribute for AzBuf derive macro

* fixes for 1.21.4

* fix examples

* update some physics code and fix ChatType

* remove unused imports in codegen

* re-add some things to migrate.py and update +mc version numbers automatically

* downgrade to 1.21.3 lol
2024-11-27 19:31:40 -06:00
mat
139d77d3c2 make item in EntityMetadataItems public
closes #169
2024-11-27 10:55:37 +00:00
mat
0817382098 replace once_cell with std:;sync::LazyLock 2024-11-27 10:26:40 +00:00
Shayne Hartford
dfdc3144b6
Update and merge the dependencies (#187)
* Add rust rover to .gitignore

* Fold dependency feature lists

* Sort dependencies alphabetically

* Update dependencies

* Upgrade dependencies

* Comment out unused dependencies

* Nightly is broken right now :)

* Fix conflict with derive_more

* cargo autoinherit to merge dependencies

* Fix clippy lints
2024-11-23 02:29:30 -06:00
Shayne Hartford
e443c5d76e
Fix debug warning (#186) 2024-11-22 21:30:01 -06:00
Shayne Hartford
a8125cd198
Add SwarmBuilder::add_account_with_opts (#185) 2024-11-20 22:43:21 -06:00
Shayne Hartford
3cf17cb896
Add more derives to the pathfinder goals for flexibility (#183) 2024-11-15 21:38:18 -06:00
Shayne Hartford
0902edb244
Switch to u32 to match other entity ids in Azalea (#182)
* Switch to u32 to match other entity ids in Azalea

* Forgot about the other entity id

* Revert "Forgot about the other entity id"

That wasn't meant for this branch, squash merge this out lol.
2024-11-12 17:59:04 -06:00
Shayne Hartford
09cdc22b86
Make some internal system handles public for use with before/after (#180) 2024-11-12 12:07:36 -06:00
Gaspard Culis
0774888a77
feat(registry): Derive Serialize and Deserialize on registry types (#179) 2024-11-03 10:58:19 -06:00
mat
6b0fe5bf63 group imports with rustfmt 2024-10-26 05:29:26 +00:00
mat
b762575db6 fix ClientboundPlayerInfoUpdatePacket and replace GameProfile with LoginFinished in an example 2024-10-26 05:28:11 +00:00
mat
a3cf78ebec add 1.21.1 to branches list in readme 2024-10-23 05:14:10 +00:00
mat
40e4096d24
1.21.2 (#171)
* partially implement 24w35a

* start updating to 24w39a + itemcomponent codegen

* fix codegen and broken packets to finish updating to 24w39a :D

* update to 1.21.2 except for blocks

* update ServerboundPlayerInputPacket impl
2024-10-23 00:08:13 -05:00
mat
abc7b43b8c Release 0.10.3+mc1.21.1
azalea@0.10.3+mc1.21.1
azalea-auth@0.10.3+mc1.21.1
azalea-block@0.10.3+mc1.21.1
azalea-block-macros@0.10.3+mc1.21.1
azalea-brigadier@0.10.3+mc1.21.1
azalea-buf@0.10.3+mc1.21.1
azalea-buf-macros@0.10.3+mc1.21.1
azalea-chat@0.10.3+mc1.21.1
azalea-client@0.10.3+mc1.21.1
azalea-core@0.10.3+mc1.21.1
azalea-crypto@0.10.3+mc1.21.1
azalea-entity@0.10.3+mc1.21.1
azalea-inventory@0.10.3+mc1.21.1
azalea-inventory-macros@0.10.3+mc1.21.1
azalea-language@0.10.3+mc1.21.1
azalea-physics@0.10.3+mc1.21.1
azalea-protocol@0.10.3+mc1.21.1
azalea-protocol-macros@0.10.3+mc1.21.1
azalea-registry@0.10.3+mc1.21.1
azalea-registry-macros@0.10.3+mc1.21.1
azalea-world@0.10.3+mc1.21.1

Generated by cargo-workspaces
2024-10-22 23:05:04 -05:00
Shayne Hartford
5e99c2218d
Add missing yaw and patch fields to ServerboundUseItemPacket (#178)
(cherry picked from commit ed7b306aff5ed1d1ce9659d41f76f3f3a6884dd6)
2024-10-22 00:24:09 -05:00
Shayne Hartford
cd2f298a62
Rename InventoryComponent to Inventory to match other components (#177)
(cherry picked from commit 266058a8d441169b46ef819595eee62337ab324c)
2024-10-21 19:21:38 -05:00
Shayne Hartford
5535877a4b
Impl FromStr for BlockPos & Vec (#174)
* Impl FromStr for BlockPos & Vec

(cherry picked from commit 288db1ca303c801c189f950fa99d5ae8369235c4)

* Add comments
2024-10-21 17:04:46 -05:00
Shayne Hartford
58d1485733
Derive Deserialize & Serialize for BlockPos & Vec3 (#175)
(cherry picked from commit f0d15d08320cbca1f3383e60cdd97efc488c3b34)
2024-10-21 14:11:51 -05:00
mat
2992fc6b47 update simdnbt 2024-09-15 06:52:16 +00:00
mat
d5a281bf15 remove debug printlns 2024-08-19 19:44:15 +00:00
mat
e485cf5501 fix incorrect comment in with_microsoft_access_token docs 2024-08-15 23:33:23 +00:00
mat
74831abbe4 modify some docs 2024-08-15 01:26:59 +00:00
mat
73091d8f93 fix sometimes being able to mine blocks through walls 2024-08-15 01:25:11 +00:00
mat
dec544a52b fix ClientboundLevelParticlesPacket errors and pathfinder /particle errors 2024-08-15 01:24:03 +00:00
Aditya Kumar
13afc1d6a4
Auth Customization Options (#159)
* Added Support for Custom Auth using `client_id` and `scope`

* fix: `Account::microsoft` and added lifetime to `Account::microsoft_with_custom_client_id`

* Added function `with_microsoft_access_token_and_custom_client_id`

* Removed Custom Scope.

* I got carried away, and made scope also customizable, later realized no customization is needed.

* Better Documentation and Minor fixes

* Added Custom Scope

* Added RpsTicket format for custom `client_id`

* Moved to non-static str

* fix example

Co-authored-by: mat <27899617+mat-1@users.noreply.github.com>

* fix doc grammer

* changed function signature

* fmt

* fixed example

* removed dead code

* Removed `d=` insertion in `client_id`

* removed unnecessary `mut`

---------

Co-authored-by: mat <27899617+mat-1@users.noreply.github.com>
2024-08-11 16:54:45 -05:00
mat
92c90753ea update readme to say 1.21.1 2024-08-11 09:04:20 +00:00
mat
5ab9c501e9 fmt 2024-08-11 09:03:57 +00:00
mat
b445b7c032 cursor_remaining was stabilized 2024-08-06 02:19:32 +00:00
Aditya Kumar
832f960531
Added Left Click Mine (1.21) (#168)
* Added Auto Mine

* Unnecessary Block Reach Check

* Added `LeftClickMine`
2024-07-21 20:17:40 -05:00
mat
ca48ed9e25 remove unwraps in events.rs 2024-07-22 01:03:36 +00:00
mat
86fd3168f7 fix panic in update_modifiers_for_held_item
found from 27cecdb8bf
2024-07-22 01:00:56 +00:00
Aditya Kumar
7e93c2d766
Made Hunger and DataComponentPatch public (#167)
* Made `Hunger` component public

* Made `DataComponentPatch` public
2024-07-20 05:45:25 -05:00
Patrick
4ee0b784ea
Determine rust channel by parsing rustc output if env vars do not exist (#163)
* Determine rust channel by parsing rustc output

The RUSTUP_TOOLCHAIN environment variable might not always be present.
This is the case for e.g. NixOS where rust is routinely not installed via
rustup, thus not setting this env var, causing build failures.
Instead, build.rs will now run `rustc -V` and check if the output contains the
word "nightly".

* Check env vars first, fall back to rustc in $PATH

* Try to check RUSTUP_TOOLCHAIN first
2024-07-19 16:28:40 -05:00
Aditya Kumar
3d717b63e5
fix: Chunk Storage Error (#158) 2024-07-12 06:24:47 -05:00
Shayne Hartford
cf4e3f609d
Add missing configuration and game connection wrappers (#161) 2024-07-12 01:07:24 -05:00
mat
ea64fba7f6
upgrade to simdnbt 0.6 (#160) 2024-07-05 00:45:45 -05:00
EnderKill98
350e32d0a9
Fix command packets (#155)
* Fix copy&paste mistake from chat command signed packet to chat command packet

* Bugfix

---------

Co-authored-by: EnderKill98 <no@mail.tld>
2024-06-16 16:03:34 -05:00
mat
f9e20fd11e slightly reword Branches section in readme 2024-06-14 00:54:25 +00:00
mat
4fc009813b add link to 1.20.6 branch in readme 2024-06-14 00:52:50 +00:00
mat
f66d2d4767
1.21 (#145)
* 24w18a (data driven enchantments not implemented yet)

* 1.21
2024-06-13 19:52:05 -05:00
mat
38eab50b4f Release 0.10.2
azalea-block@0.10.2
azalea-block-macros@0.10.2
azalea-brigadier@0.10.2
azalea-buf@0.10.2
azalea-chat@0.10.2
azalea-client@0.10.2
azalea-core@0.10.2
azalea-entity@0.10.2
azalea-inventory@0.10.2
azalea-physics@0.10.2
azalea-protocol@0.10.2
azalea-registry@0.10.2
azalea-world@0.10.2

Generated by cargo-workspaces
2024-06-13 19:22:11 -05:00
mat
7b5ed58b9b fix generated block field names so tests pass again 2024-06-14 00:19:22 +00:00
mat
dd2f0465a4 fix incorrect varlong reader 2024-06-11 09:48:11 +00:00
mat
c7f9dc4b3d remove all references to clean_property_name 2024-05-23 03:29:37 +00:00
mat
22baedb24b change property names to be unique and to match the minecraft ids 2024-05-23 03:24:12 +00:00
mat
4f6f104ddb resolve clippy warning 2024-05-23 02:12:34 +00:00
urisinger
f9c28ca5fa
make getting shapes faster (#149)
* supposed to make getting shapes faster

* why was this reversed

* forgot to run codegen

* don't panic when getting the shape for invalid block ids

---------

Co-authored-by: mat <git@matdoes.dev>
2024-05-22 21:08:15 -05:00
mat
73bcc6639b fix another edge case in FormattedText::from_nbt_tag that happens with viaversion 2024-05-22 10:28:43 +00:00
mat
729d211406 read Tool blocks as HolderSet
fixes an error with viaversion servers
2024-05-22 10:14:35 +00:00
veronoicc
892869ad12
Update simdnbt dep (#148)
* Update simdnbt dep

* Fix not compiling (rust analyzer is still yapping, i trust you cargo check)

* Update simdnbt to 0.5.2

* finally everything works :prayge:
2024-05-14 16:59:36 -05:00
mat
b00106b7ff update simdnbt 2024-05-14 09:13:34 +00:00
EightFactorial
8138d388e7
Add a brigadier example for use inside a bevy App (#146) 2024-05-09 21:55:55 -05:00
mat
afcf497989 link 1.20.4 branch in readme 2024-05-03 06:42:25 +00:00
mat
893f837b68 extremely complicated update to 1.20.6 2024-05-01 05:19:54 +00:00
mat
8e2e81651c fmt 2024-04-27 06:08:38 +00:00
Luis
6553d9510d
Use ClientIntention over ConnectionProtocol for ClientIntentionPacket (#143)
* fix!: use ClientIntention over ConnectionProtocol for ClientIntentionPacket

* chore: remove McBufRead/Writable from ConnectionProtocol

* chore: use From over Into for ClientIntention to ConnectionProtocol conversion

* chore: organise imports in existing style
2024-04-27 01:03:20 -05:00
mat
84f66a55a5 Release 0.10.1
azalea@0.10.1
azalea-auth@0.10.1
azalea-buf@0.10.1
azalea-chat@0.10.1
azalea-client@0.10.1
azalea-entity@0.10.1
azalea-inventory@0.10.1
azalea-protocol@0.10.1
azalea-world@0.10.1

Generated by cargo-workspaces
2024-04-24 17:56:35 -05:00
mat
a3169d8ffe fix some deps 2024-04-24 22:43:44 +00:00
mat
e7c5a5e4fa require azalea-buf feature for azalea-chat in some places 2024-04-24 05:05:19 +00:00
mat
287f493172 Release independent packages
Generated by cargo-workspaces
2024-04-24 00:01:16 -05:00
mat
2f51a0a123 Release 0.10.0
azalea@0.10.0
azalea-auth@0.10.0
azalea-block@0.10.0
azalea-block-macros@0.10.0
azalea-brigadier@0.10.0
azalea-buf@0.10.0
azalea-buf-macros@0.10.0
azalea-chat@0.10.0
azalea-client@0.10.0
azalea-core@0.10.0
azalea-crypto@0.10.0
azalea-entity@0.10.0
azalea-inventory@0.10.0
azalea-inventory-macros@0.10.0
azalea-language@0.10.0
azalea-physics@0.10.0
azalea-protocol@0.10.0
azalea-protocol-macros@0.10.0
azalea-registry@0.10.0
azalea-registry-macros@0.10.0
azalea-world@0.10.0

Generated by cargo-workspaces
2024-04-23 23:56:30 -05:00
mat
df8068b79c upgrade deps 2024-04-24 04:20:39 +00:00
mat
7357455cad Revert "Release 0.10.0"
This reverts commit f26aa56f42.
2024-04-24 04:11:53 +00:00
mat
f26aa56f42 Release 0.10.0
azalea@0.10.0
azalea-auth@0.10.0
azalea-block@0.10.0
azalea-block-macros@0.10.0
azalea-brigadier@0.10.0
azalea-buf@0.10.0
azalea-buf-macros@0.10.0
azalea-chat@0.10.0
azalea-client@0.10.0
azalea-core@0.10.0
azalea-crypto@0.10.0
azalea-entity@0.10.0
azalea-inventory@0.10.0
azalea-inventory-macros@0.10.0
azalea-language@0.10.0
azalea-physics@0.10.0
azalea-protocol@0.10.0
azalea-protocol-macros@0.10.0
azalea-registry@0.10.0
azalea-registry-macros@0.10.0
azalea-world@0.10.0

Generated by cargo-workspaces
2024-04-23 23:11:11 -05:00
mat
357824014e fix ClientboundLevelParticlesPacket 2024-04-24 03:58:26 +00:00
mat
79a0d58cc5 fix warning when entities that didn't move chunks despawn 2024-04-24 03:11:26 +00:00
mat
3b635c1d6d sort generated tags 2024-04-23 21:16:18 -05:00
mat
1d80f531b7
1.20.5 (#127)
* 23w51b

* make recalculate_near_end_of_path public

so other plugins can do .after(recalculate_near_end_of_path)

* update to 24w03a i think

* start implementing 24w13a

* registries work (but a lot of packets are still broken)

* fix recipes and commands packets

* i love codecs :D i am not going insane :D mojang's java is very readable :D

* item components are "implemented" meowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeowmeow

* update to 1.20.5-pre3

* fix all the broken packets and clippy (mojang please don't do an update like this again or i will murder someone)

* 1.20.5-rc1

* fix failing tests

* 1.20.5
2024-04-23 10:34:50 -05:00
mat
0ddad8bd9c fix edge case with reading FormattedText as nbt 2024-04-20 21:06:37 +00:00
mat
f919fb65d6 upgrade deps 2024-04-20 04:12:16 +00:00
mat
8a1e1b7bb9 clippy 2024-04-20 04:03:03 +00:00
mat
6d9d1a4569 add Client::join_with_proxy and fix tests 2024-04-20 03:59:50 +00:00
mat
353eda21ac socks5 support (#113) 2024-04-20 03:40:59 +00:00
764 changed files with 57684 additions and 34205 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.

0
.github/workflows/check.yml vendored Executable file → Normal file
View file

View file

@ -1,45 +1,44 @@
name: Doc
on:
push:
branches:
- main
workflow_dispatch:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
pages: write
id-token: write
contents: write
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
group: "pages"
cancel-in-progress: true
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
- run: cargo doc --workspace --no-deps
- uses: "finnp/create-file-action@master"
env:
FILE_NAME: "./target/doc/index.html"
FILE_DATA: '<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;url=''./azalea''"/></head></html>' # Redirect to default page
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: './target/doc/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
- run: cargo doc --workspace --no-deps
- uses: "finnp/create-file-action@master"
env:
FILE_NAME: "./target/doc/index.html"
FILE_DATA: '<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;url=''./azalea''"/></head></html>' # Redirect to default page
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "./target/doc/"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

12
.gitignore vendored Executable file → Normal file
View file

@ -1,6 +1,7 @@
/target
/doc
.vscode
.idea/
.vscode/
doc/
target/
.cargo/config
.cargo/config.toml
@ -14,3 +15,8 @@ flamegraph.svg
perf.data
perf.data.old
heaptrack.*
rustc-ice-*
# not created by azalea itself, sometimes used for debugging since the docs specifically mentions using azalea.log
azalea.log

0
.gitpod.yml Executable file → Normal file
View file

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.

3018
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,111 @@
[workspace]
members = [
"azalea",
"azalea-client",
"azalea-protocol",
"azalea-chat",
"azalea-core",
"azalea-auth",
"azalea-brigadier",
"azalea-crypto",
"azalea-world",
"azalea-language",
"azalea-block",
"azalea-buf",
"azalea-physics",
"azalea-registry",
"azalea-inventory",
"azalea-entity",
"azalea",
"azalea-auth",
"azalea-block",
"azalea-brigadier",
"azalea-buf",
"azalea-chat",
"azalea-client",
"azalea-core",
"azalea-crypto",
"azalea-entity",
"azalea-inventory",
"azalea-language",
"azalea-physics",
"azalea-protocol",
"azalea-registry",
"azalea-world",
]
resolver = "2"
# --- Workspace Settings ---
[workspace.package]
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.98"
async-recursion = "1.1.1"
base64 = "0.22.1"
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.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.2", features = ["zlib-rs"] }
futures = "0.3.31"
futures-lite = "2.6.0"
md-5 = "0.10.6"
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.4"
proc-macro2 = "1.0.95"
quote = "1.0.40"
rand = "0.9.2"
regex = "1.11.1"
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.142"
sha-1 = "0.10.1"
sha2 = "0.11.0-rc.0"
socks5-impl = "0.7.2"
syn = "2.0.104"
thiserror = "2.0.12"
tokio = "1.47.1"
tokio-util = "0.7.15"
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
hickory-resolver = "0.25.2"
uuid = "1.17"
num-format = "0.4.4"
indexmap = "2.10.0"
paste = "1.0.15"
compact_str = "0.9.0"
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 ---
[profile.release]
debug = true

37
README.md Executable file → Normal file
View file

@ -8,23 +8,23 @@ A collection of Rust crates for making Minecraft bots, clients, and tools.
<img src="https://github.com/azalea-rs/azalea/assets/27899617/b98a42df-5cf0-4d1f-ae7c-ecca333e3cab" alt="Azalea" height="200">
</p>
<!-- The line below is automatically read and updated by the migrate script, so don't change it manually. -->
_Currently supported Minecraft version: `1.20.4`._
_Currently supported Minecraft version: `1.21.8`._
> [!WARNING]
> Azalea is still very unfinished, though most crates are in a somewhat 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 collisions and water physics 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
@ -49,12 +49,23 @@ If you'd like to chat about Azalea, you can join the Matrix space at [#azalea:ma
- Bedrock edition.
- Graphics.
## Branches
## Real-world bots using Azalea
There are several branches in the Azalea repository that target older Minecraft versions. It is not guaranteed that they will be up-to-date with the latest version of Azalea. If you'd like to update them or add more, please open a PR.
Here's an incomplete list of bots built using Azalea, primarily intended as a reference in addition to the existing documentation and examples:
- [1.20.2](https://github.com/azalea-rs/azalea/tree/1.20.2)
- [1.20-1.20.1](https://github.com/azalea-rs/azalea/tree/1.20.1)
- [1.19.4](https://github.com/azalea-rs/azalea/tree/1.19.4)
- [1.19.3](https://github.com/azalea-rs/azalea/tree/1.19.3)
- [1.19.2](https://github.com/azalea-rs/azalea/tree/1.19.2)
- [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.
- [ErrorNoInternet/ErrorNoWatcher](https://github.com/ErrorNoInternet/ErrorNoWatcher) - A Minecraft bot with Lua scripting support.
You can see more projects built with Azalea in the [GitHub dependency graph](https://github.com/azalea-rs/azalea/network/dependents).
## Plugins
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.
If you've created your own plugin for Azalea, please create a PR to add it to this list :).

View file

@ -1,33 +1,29 @@
[package]
description = "A port of Mojang's Authlib and launcher authentication."
edition = "2021"
license = "MIT"
name = "azalea-auth"
repository = "https://github.com/azalea-rs/azalea/tree/main/azalea-auth"
version = "0.9.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
description = "A port of Mojang's Authlib and launcher authentication."
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "0.9.0" }
azalea-crypto = { path = "../azalea-crypto", version = "0.9.0" }
base64 = "0.21.7"
chrono = { version = "0.4.34", default-features = false, features = ["serde"] }
tracing = "0.1.40"
num-bigint = "0.4.4"
once_cell = "1.19.0"
reqwest = { version = "0.11.24", default-features = false, features = [
azalea-buf.workspace = true
azalea-crypto.workspace = true
base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
md-5.workspace = true
reqwest = { workspace = true, default-features = false, features = [
"json",
"rustls-tls",
] }
rsa = "0.9.6"
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
thiserror = "1.0.57"
tokio = { version = "1.36.0", features = ["fs"] }
uuid = { version = "1.7.0", features = ["serde", "v3"] }
md-5 = "0.10.6"
rsa.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["fs"] }
tracing.workspace = true
uuid = { workspace = true, features = ["serde", "v3"] }
[dev-dependencies]
env_logger = "0.11.2"
tokio = { version = "1.36.0", features = ["full"] }
env_logger.workspace = true
tokio = { workspace = true, features = ["full"] }

4
azalea-auth/README.md Executable file → Normal file
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.vg contributors](https://wiki.vg/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).

0
azalea-auth/examples/auth.rs Executable file → Normal file
View file

5
azalea-auth/examples/auth_manual.rs Executable file → Normal file
View file

@ -18,15 +18,16 @@ async fn main() -> Result<(), Box<dyn Error>> {
Ok(())
}
// We will be using default `client_id` and `scope`
async fn auth() -> Result<ProfileResponse, Box<dyn Error>> {
let client = reqwest::Client::new();
let res = azalea_auth::get_ms_link_code(&client).await?;
let res = azalea_auth::get_ms_link_code(&client, None, None).await?;
println!(
"Go to {} and enter the code {}",
res.verification_uri, res.user_code
);
let msa = azalea_auth::get_ms_auth_token(&client, res).await?;
let msa = azalea_auth::get_ms_auth_token(&client, res, None).await?;
let auth_result = azalea_auth::get_minecraft_token(&client, &msa.data.access_token).await?;
Ok(azalea_auth::get_profile(&client, &auth_result.minecraft_access_token).await?)
}

0
azalea-auth/examples/certificates.rs Executable file → Normal file
View file

150
azalea-auth/src/auth.rs Executable file → Normal file
View file

@ -1,19 +1,23 @@
//! Handle Minecraft (Xbox) authentication.
use crate::cache::{self, CachedAccount, ExpiringValue};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
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;
use crate::cache::{self, CachedAccount, ExpiringValue};
#[derive(Default)]
pub struct AuthOpts {
pub struct AuthOpts<'a> {
/// Whether we should check if the user actually owns the game. This will
/// fail if the user has Xbox Game Pass! Note that this isn't really
/// necessary, since getting the user profile will check this anyways.
@ -24,6 +28,12 @@ pub struct AuthOpts {
/// The directory to store the cache in. If this is not set, caching is not
/// done.
pub cache_file: Option<PathBuf>,
/// If you choose to use your own Microsoft authentication instead of using
/// Nintendo Switch, just put your client_id here.
pub client_id: Option<&'a str>,
/// If you want to use custom scope instead of default one, just put your
/// scope here.
pub scope: Option<&'a str>,
}
#[derive(Debug, Error)]
@ -59,15 +69,16 @@ pub enum AuthError {
/// If you want to use your own code to cache or show the auth code to the user
/// in a different way, use [`get_ms_link_code`], [`get_ms_auth_token`],
/// [`get_minecraft_token`] and [`get_profile`] instead.
pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError> {
pub async fn auth(email: &str, opts: AuthOpts<'_>) -> Result<AuthResult, AuthError> {
let cached_account = if let Some(cache_file) = &opts.cache_file {
cache::get_account_in_cache(cache_file, email).await
} else {
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 :)
@ -76,26 +87,38 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError>
profile: account.profile.clone(),
})
} else {
let client_id = opts.client_id.unwrap_or(CLIENT_ID);
let scope = opts.scope.unwrap_or(SCOPE);
let client = reqwest::Client::new();
let mut msa = if let Some(account) = cached_account {
account.msa
} else {
interactive_get_ms_auth_token(&client, email).await?
interactive_get_ms_auth_token(&client, email, Some(client_id), Some(scope)).await?
};
if msa.is_expired() {
tracing::trace!("refreshing Microsoft auth token");
match refresh_ms_auth_token(&client, &msa.data.refresh_token).await {
trace!("refreshing Microsoft auth token");
match refresh_ms_auth_token(
&client,
&msa.data.refresh_token,
opts.client_id,
opts.scope,
)
.await
{
Ok(new_msa) => msa = new_msa,
Err(e) => {
// can't refresh, ask the user to auth again
tracing::error!("Error refreshing Microsoft auth token: {}", e);
msa = interactive_get_ms_auth_token(&client, email).await?;
error!("Error refreshing Microsoft auth token: {}", e);
msa =
interactive_get_ms_auth_token(&client, email, Some(client_id), Some(scope))
.await?;
}
}
}
let msa_token = &msa.data.access_token;
tracing::trace!("Got access token: {msa_token}");
trace!("Got access token: {msa_token}");
let res = get_minecraft_token(&client, msa_token).await?;
@ -108,8 +131,8 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError>
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 {
@ -121,9 +144,8 @@ pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError>
},
)
.await
{
tracing::error!("{}", e);
}
{
error!("{}", e);
}
Ok(AuthResult {
@ -259,6 +281,7 @@ pub struct ProfileResponse {
// nintendo switch (so it works for accounts that are under 18 years old)
const CLIENT_ID: &str = "00000000441cc96b";
const SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL";
#[derive(Debug, Error)]
pub enum GetMicrosoftAuthTokenError {
@ -280,12 +303,12 @@ pub enum GetMicrosoftAuthTokenError {
///
/// ```
/// # async fn example(client: &reqwest::Client) -> Result<(), Box<dyn std::error::Error>> {
/// let res = azalea_auth::get_ms_link_code(&client).await?;
/// let res = azalea_auth::get_ms_link_code(&client, None, None).await?;
/// println!(
/// "Go to {} and enter the code {}",
/// res.verification_uri, res.user_code
/// );
/// let msa = azalea_auth::get_ms_auth_token(client, res).await?;
/// let msa = azalea_auth::get_ms_auth_token(client, res, None).await?;
/// let minecraft = azalea_auth::get_minecraft_token(client, &msa.data.access_token).await?;
/// let profile = azalea_auth::get_profile(&client, &minecraft.minecraft_access_token).await?;
/// # Ok(())
@ -293,12 +316,22 @@ pub enum GetMicrosoftAuthTokenError {
/// ```
pub async fn get_ms_link_code(
client: &reqwest::Client,
client_id: Option<&str>,
scope: Option<&str>,
) -> Result<DeviceCodeResponse, GetMicrosoftAuthTokenError> {
let client_id = if let Some(c) = client_id {
c
} else {
CLIENT_ID
};
let scope = if let Some(c) = scope { c } else { SCOPE };
Ok(client
.post("https://login.live.com/oauth20_connect.srf")
.form(&vec![
("scope", "service::user.auth.xboxlive.com::MBI_SSL"),
("client_id", CLIENT_ID),
.form(&[
("scope", scope),
("client_id", client_id),
("response_type", "device_code"),
])
.send()
@ -314,30 +347,37 @@ pub async fn get_ms_link_code(
pub async fn get_ms_auth_token(
client: &reqwest::Client,
res: DeviceCodeResponse,
client_id: Option<&str>,
) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> {
let login_expires_at = Instant::now() + std::time::Duration::from_secs(res.expires_in);
let client_id = if let Some(c) = client_id {
c
} else {
CLIENT_ID
};
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;
tracing::trace!("Polling to check if user has logged in...");
if let Ok(access_token_response) = client
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}"
"https://login.live.com/oauth20_token.srf?client_id={client_id}"
))
.form(&vec![
("client_id", CLIENT_ID),
.form(&[
("client_id", client_id),
("device_code", &res.device_code),
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
])
.send()
.await?
.json::<AccessTokenResponse>()
.await
{
tracing::trace!("access_token_response: {:?}", access_token_response);
let expires_at = SystemTime::now()
+ std::time::Duration::from_secs(access_token_response.expires_in);
.await;
if let Ok(access_token_response) = res {
trace!("access_token_response: {:?}", access_token_response);
let expires_at =
SystemTime::now() + Duration::from_secs(access_token_response.expires_in);
return Ok(ExpiringValue {
data: access_token_response,
expires_at: expires_at
@ -357,15 +397,17 @@ pub async fn get_ms_auth_token(
pub async fn interactive_get_ms_auth_token(
client: &reqwest::Client,
email: &str,
client_id: Option<&str>,
scope: Option<&str>,
) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> {
let res = get_ms_link_code(client).await?;
tracing::trace!("Device code response: {:?}", res);
let res = get_ms_link_code(client, client_id, scope).await?;
trace!("Device code response: {:?}", res);
println!(
"Go to \x1b[1m{}\x1b[m and enter the code \x1b[1m{}\x1b[m for \x1b[1m{}\x1b[m",
res.verification_uri, res.user_code, email
);
get_ms_auth_token(client, res).await
get_ms_auth_token(client, res, client_id).await
}
#[derive(Debug, Error)]
@ -379,12 +421,17 @@ pub enum RefreshMicrosoftAuthTokenError {
pub async fn refresh_ms_auth_token(
client: &reqwest::Client,
refresh_token: &str,
client_id: Option<&str>,
scope: Option<&str>,
) -> Result<ExpiringValue<AccessTokenResponse>, RefreshMicrosoftAuthTokenError> {
let client_id = client_id.unwrap_or(CLIENT_ID);
let scope = scope.unwrap_or(SCOPE);
let access_token_response_text = client
.post("https://login.live.com/oauth20_token.srf")
.form(&vec![
("scope", "service::user.auth.xboxlive.com::MBI_SSL"),
("client_id", CLIENT_ID),
.form(&[
("scope", scope),
("client_id", client_id),
("grant_type", "refresh_token"),
("refresh_token", refresh_token),
])
@ -395,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
@ -430,7 +476,7 @@ async fn auth_with_xbox_live(
"TokenType": "JWT"
});
let payload = auth_json.to_string();
tracing::trace!("auth_json: {:#?}", auth_json);
trace!("auth_json: {:#?}", auth_json);
let res = client
.post("https://user.auth.xboxlive.com/user/authenticate")
.header("Content-Type", "application/json")
@ -443,7 +489,7 @@ async fn auth_with_xbox_live(
.await?
.json::<XboxLiveAuthResponse>()
.await?;
tracing::trace!("Xbox Live auth response: {:?}", res);
trace!("Xbox Live auth response: {:?}", res);
// not_after looks like 2020-12-21T19:52:08.4463796Z
let expires_at = DateTime::parse_from_rfc3339(&res.not_after)
@ -484,7 +530,7 @@ async fn obtain_xsts_for_minecraft(
.await?
.json::<XboxLiveAuthResponse>()
.await?;
tracing::trace!("Xbox Live auth response (for XSTS): {:?}", res);
trace!("Xbox Live auth response (for XSTS): {:?}", res);
Ok(res.token)
}
@ -510,9 +556,9 @@ async fn auth_with_minecraft(
.await?
.json::<MinecraftAuthResponse>()
.await?;
tracing::trace!("{:?}", res);
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
@ -537,7 +583,7 @@ pub async fn check_ownership(
.await?
.json::<GameOwnershipResponse>()
.await?;
tracing::trace!("{:?}", res);
trace!("{:?}", res);
// vanilla checks here to make sure the signatures are right, but it's not
// actually required so we just don't
@ -562,7 +608,7 @@ pub async fn get_profile(
.await?
.json::<ProfileResponse>()
.await?;
tracing::trace!("{:?}", res);
trace!("{:?}", res);
Ok(res)
}

29
azalea-auth/src/cache.rs Executable file → Normal file
View file

@ -1,20 +1,27 @@
//! Cache auth information
use std::{
io,
path::Path,
time::{SystemTime, UNIX_EPOCH},
};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
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),
}
@ -82,17 +89,19 @@ async fn get_entire_cache(cache_file: &Path) -> Result<Vec<CachedAccount>, Cache
Ok(cache)
}
async fn set_entire_cache(cache_file: &Path, cache: Vec<CachedAccount>) -> Result<(), CacheError> {
tracing::trace!("saving cache: {:?}", cache);
trace!("saving cache: {:?}", cache);
if !cache_file.exists() {
let cache_file_parent = cache_file
.parent()
.expect("Cache file is root directory and also doesn't exist.");
tracing::debug!(
debug!(
"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

@ -1,8 +1,9 @@
use base64::Engine;
use chrono::{DateTime, Utc};
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
use rsa::{RsaPrivateKey, pkcs8::DecodePrivateKey};
use serde::Deserialize;
use thiserror::Error;
use tracing::trace;
#[derive(Debug, Error)]
pub enum FetchCertificatesError {
@ -26,7 +27,7 @@ pub async fn fetch_certificates(
.await?
.json::<CertificatesResponse>()
.await?;
tracing::trace!("{:?}", res);
trace!("{:?}", res);
// using RsaPrivateKey::from_pkcs8_pem gives an error with decoding base64 so we
// just decode it ourselves

26
azalea-auth/src/game_profile.rs Executable file → Normal file
View file

@ -1,15 +1,17 @@
use azalea_buf::McBuf;
use std::{collections::HashMap, sync::Arc};
use azalea_buf::AzBuf;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(McBuf, Debug, Clone, Default, Eq, PartialEq)]
#[derive(AzBuf, Debug, Clone, Default, Eq, PartialEq)]
pub struct GameProfile {
/// The UUID of the player.
pub uuid: Uuid,
/// The username of the player.
pub name: String,
pub properties: HashMap<String, ProfilePropertyValue>,
// this is an arc to make GameProfile cheaper to clone when the properties are big
pub properties: Arc<HashMap<String, ProfilePropertyValue>>,
}
impl GameProfile {
@ -17,7 +19,7 @@ impl GameProfile {
GameProfile {
uuid,
name,
properties: HashMap::new(),
properties: Arc::new(HashMap::new()),
}
}
}
@ -37,12 +39,12 @@ impl From<SerializableGameProfile> for GameProfile {
Self {
uuid: value.id,
name: value.name,
properties,
properties: Arc::new(properties),
}
}
}
#[derive(McBuf, Debug, Clone, Eq, PartialEq)]
#[derive(AzBuf, Debug, Clone, Eq, PartialEq)]
pub struct ProfilePropertyValue {
pub value: String,
pub signature: Option<String>,
@ -58,11 +60,11 @@ pub struct SerializableGameProfile {
impl From<GameProfile> for SerializableGameProfile {
fn from(value: GameProfile) -> Self {
let mut properties = Vec::new();
for (key, value) in value.properties {
for (key, value) in &*value.properties {
properties.push(SerializableProfilePropertyValue {
name: key,
value: value.value,
signature: value.signature,
name: key.clone(),
value: value.value.clone(),
signature: value.signature.clone(),
});
}
Self {
@ -113,7 +115,7 @@ mod tests {
signature: Some("zxcv".to_string()),
},
);
map
map.into()
},
}
);

0
azalea-auth/src/lib.rs Executable file → Normal file
View file

7
azalea-auth/src/sessionserver.rs Executable file → Normal file
View file

@ -1,5 +1,6 @@
//! Tell Mojang you're joining a multiplayer server.
use once_cell::sync::Lazy;
use std::sync::LazyLock;
use reqwest::StatusCode;
use serde::Deserialize;
use serde_json::json;
@ -49,7 +50,7 @@ pub struct ForbiddenError {
pub path: String,
}
static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(reqwest::Client::new);
static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(reqwest::Client::new);
/// Tell Mojang's servers that you are going to join a multiplayer server,
/// which is required to join online-mode servers. The server ID is an empty
@ -158,7 +159,7 @@ pub async fn serverside_auth(
StatusCode::FORBIDDEN => {
return Err(ServerSessionServerError::Unknown(
res.json::<ForbiddenError>().await?.error,
))
));
}
status_code => {
// log the headers

View file

@ -1,16 +1,12 @@
[package]
description = "Representation of Minecraft block states."
edition = "2021"
license = "MIT"
name = "azalea-block"
repository = "https://github.com/azalea-rs/azalea/tree/main/azalea-block"
version = "0.9.1"
[lib]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
description = "Representation of Minecraft block states."
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
azalea-block-macros = { path = "./azalea-block-macros", version = "0.9.0" }
azalea-buf = { path = "../azalea-buf", version = "0.9.0" }
azalea-registry = { version = "0.9.0", path = "../azalea-registry" }
azalea-block-macros.workspace = true
azalea-buf.workspace = true
azalea-registry.workspace = true

22
azalea-block/README.md Executable file → Normal file
View file

@ -9,10 +9,10 @@ There's three block types, used for different things. You can (mostly) convert b
```
# use azalea_block::BlockState;
let block_state: BlockState = azalea_block::blocks::CobblestoneWall {
east: azalea_block::properties::EastWall::Low,
north: azalea_block::properties::NorthWall::Low,
south: azalea_block::properties::SouthWall::Low,
west: azalea_block::properties::WestWall::Low,
east: azalea_block::properties::WallEast::Low,
north: azalea_block::properties::WallNorth::Low,
south: azalea_block::properties::WallSouth::Low,
west: azalea_block::properties::WallWest::Low,
up: false,
waterlogged: false,
}
@ -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

@ -1,17 +1,15 @@
[package]
description = "Proc macros used by azalea-block."
edition = "2021"
license = "MIT"
name = "azalea-block-macros"
repository = "https://github.com/azalea-rs/azalea/tree/main/azalea-block/azalea-block-macros"
version = "0.9.1"
description = "Proc macros used by azalea-block."
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
proc-macro = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "1.0.78"
quote = "1.0.35"
syn = "2.0.49"
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true

156
azalea-block/azalea-block-macros/src/lib.rs Executable file → Normal file
View file

@ -2,22 +2,24 @@
mod utils;
use std::{collections::HashMap, fmt::Write};
use proc_macro::TokenStream;
use proc_macro2::TokenTree;
use quote::quote;
use std::collections::HashMap;
use std::fmt::Write;
use syn::{
braced,
Expr, Ident, LitStr, Token, braced,
ext::IdentExt,
parenthesized,
parse::{Parse, ParseStream, Result},
parse_macro_input,
punctuated::Punctuated,
token, Expr, Ident, LitStr, Token,
token,
};
use utils::{combinations_of, to_pascal_case};
// must be the same as the type in `azalea-block/src/lib.rs`
type BlockStateIntegerRepr = u16;
enum PropertyType {
/// `Axis { X, Y, Z }`
Enum {
@ -39,11 +41,14 @@ struct PropertyDefinitions {
properties: Vec<PropertyDefinition>,
}
/// `snowy: Snowy(false)` or `axis: properties::Axis::Y`
/// `"snowy": Snowy(false)` or `"axis": properties::Axis::Y`
#[derive(Debug)]
struct PropertyWithNameAndDefault {
// "snowy" / "axis"
name: Ident,
// "snowy" "axis"
name: String,
/// The property name, potentially modified so it works better as a struct
/// field.
name_ident: Ident,
// Snowy / Axis
property_type: Ident,
property_value_type: Ident,
@ -54,7 +59,7 @@ struct PropertyWithNameAndDefault {
/// ```ignore
/// grass_block => BlockBehavior::default(), {
/// snowy: false,
/// "snowy": false,
/// },
/// ```
struct BlockDefinition {
@ -64,8 +69,8 @@ struct BlockDefinition {
}
impl Parse for PropertyWithNameAndDefault {
fn parse(input: ParseStream) -> Result<Self> {
// `snowy: Snowy(false)` or `axis: properties::Axis::Y`
let property_name = input.parse()?;
// `"snowy": Snowy(false)` or `"axis": properties::Axis::Y`
let property_name = input.parse::<LitStr>()?.value();
input.parse::<Token![:]>()?;
let first_ident = input.call(Ident::parse_any)?;
@ -100,8 +105,11 @@ impl Parse for PropertyWithNameAndDefault {
}
};
let property_name_ident = name_to_ident(&property_name);
Ok(PropertyWithNameAndDefault {
name: property_name,
name_ident: property_name_ident,
property_type,
property_value_type,
is_enum,
@ -196,11 +204,11 @@ impl Parse for PropertyDefinitions {
impl Parse for BlockDefinition {
fn parse(input: ParseStream) -> Result<Self> {
// acacia_button => BlockBehavior::default(), {
// Facing=North,
// Powered=False,
// Face=Wall,
// }
// acacia_button => BlockBehavior::new().strength(0.5, 0.5), {
// "face": Face::Wall,
// "facing": FacingCardinal::North,
// "powered": Powered(false),
// },
let name = input.parse()?;
input.parse::<Token![=>]>()?;
let behavior = input.parse()?;
@ -268,7 +276,7 @@ impl Parse for MakeBlockStates {
}
struct PropertyVariantData {
pub block_state_ids: Vec<u32>,
pub block_state_ids: Vec<BlockStateIntegerRepr>,
pub ident: Ident,
pub is_enum: bool,
}
@ -281,7 +289,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let mut properties_map = HashMap::new();
let mut property_struct_names_to_names = HashMap::new();
let mut state_id: u32 = 0;
let mut state_id: BlockStateIntegerRepr = 0;
for property in &input.property_definitions.properties {
let property_struct_name: Ident;
@ -332,8 +340,8 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
#property_enum_variants
}
impl From<u32> for #property_struct_name {
fn from(value: u32) -> Self {
impl From<crate::block_state::BlockStateIntegerRepr> for #property_struct_name {
fn from(value: crate::block_state::BlockStateIntegerRepr) -> Self {
match value {
#property_from_number_variants
_ => panic!("Invalid property value: {}", value),
@ -351,8 +359,8 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct #property_struct_name(pub bool);
impl From<u32> for #property_struct_name {
fn from(value: u32) -> Self {
impl From<crate::block_state::BlockStateIntegerRepr> for #property_struct_name {
fn from(value: crate::block_state::BlockStateIntegerRepr) -> Self {
match value {
0 => Self(false),
1 => Self(true),
@ -416,29 +424,25 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
Some(
previous_names
.iter()
.filter(|&p| p == &property.name.to_string())
.filter(|&p| p == &property.name)
.count(),
)
} else {
None
};
// ```ignore
// let mut property_name = property_struct_names_to_names
// .get(&property.property_type.to_string())
// .unwrap_or_else(|| panic!("Property '{}' is bad", property.property_type))
// .clone();
// ```
let mut property_name = property_struct_names_to_names
.get(&property.name.to_string())
.get(&property.name)
.cloned()
.unwrap_or_else(|| property.name.to_string());
.unwrap_or_else(|| property.name.clone());
previous_names.push(property_name.clone());
if let Some(index) = index {
// property_name.push_str(&format!("_{}", &index.to_string()));
write!(property_name, "_{index}").unwrap();
}
properties_with_name.push(PropertyWithNameAndDefault {
name: Ident::new(&property_name, proc_macro2::Span::call_site()),
name_ident: name_to_ident(&property_name),
name: property_name,
property_type: property.property_type.clone(),
property_value_type: property.property_value_type.clone(),
is_enum: property.is_enum,
@ -457,17 +461,15 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let mut block_struct_fields = quote! {};
for PropertyWithNameAndDefault {
property_value_type,
name,
name_ident,
is_enum,
..
} in &properties_with_name
{
// let property_name_snake =
// Ident::new(&property.to_string(), proc_macro2::Span::call_site());
block_struct_fields.extend(if *is_enum {
quote! { pub #name: properties::#property_value_type, }
quote! { pub #name_ident: properties::#property_value_type, }
} else {
quote! { pub #name: #property_value_type, }
quote! { pub #name_ident: #property_value_type, }
});
}
@ -502,19 +504,19 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let mut from_block_to_state_combination_match_inner = quote! {};
for i in 0..properties_with_name.len() {
let property = &properties_with_name[i];
let property_name = &property.name;
let property_name_ident = &property.name_ident;
let property_value_name_ident = &property.property_type;
let variant =
Ident::new(&combination[i].to_string(), proc_macro2::Span::call_site());
// this terrible code just gets the property default as a string
let property_default_as_string = if let TokenTree::Ident(ident) =
property.default.clone().into_iter().last().unwrap()
{
ident.to_string()
} else {
panic!()
};
let property_default_as_string =
match property.default.clone().into_iter().last().unwrap() {
TokenTree::Ident(ident) => ident.to_string(),
_ => {
panic!()
}
};
if property_default_as_string != combination[i] {
is_default = false;
}
@ -526,7 +528,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
};
from_block_to_state_combination_match_inner.extend(quote! {
#property_name: #property_variant,
#property_name_ident: #property_variant,
});
// add to properties_to_state_ids
@ -549,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 {
@ -562,15 +564,16 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let Some(default_state_id) = default_state_id else {
let defaults = properties_with_name
.iter()
.map(|p| {
if let TokenTree::Ident(i) = p.default.clone().into_iter().last().unwrap() {
i.to_string()
} else {
.map(|p| match p.default.clone().into_iter().last().unwrap() {
TokenTree::Ident(i) => i.to_string(),
_ => {
panic!()
}
})
.collect::<Vec<_>>();
panic!("Couldn't get default state id for {block_name_pascal_case}, combinations={block_properties_vec:?}, defaults={defaults:?}")
panic!(
"Couldn't get default state id for {block_name_pascal_case}, combinations={block_properties_vec:?}, defaults={defaults:?}"
)
};
// 7035..=7058 => {
@ -582,17 +585,17 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
// }
// }
let mut from_state_to_block_inner = quote! {};
let mut division = 1u32;
let mut division: BlockStateIntegerRepr = 1;
for i in (0..properties_with_name.len()).rev() {
let PropertyWithNameAndDefault {
property_type: property_struct_name_ident,
name: property_name,
name_ident: property_name_ident,
property_value_type,
..
} = &properties_with_name[i];
let property_variants = &block_properties_vec[i];
let property_variants_count = property_variants.len() as u32;
let property_variants_count = property_variants.len() as crate::BlockStateIntegerRepr;
let conversion_code = {
if &property_value_type.to_string() == "bool" {
assert_eq!(property_variants_count, 2);
@ -603,7 +606,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
}
};
from_state_to_block_inner.extend(quote! {
#property_name: #conversion_code,
#property_name_ident: #conversion_code,
});
division *= property_variants_count;
@ -622,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),
@ -630,19 +633,19 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let mut block_default_fields = quote! {};
for PropertyWithNameAndDefault {
name,
name_ident,
default: property_default,
..
} in properties_with_name
{
block_default_fields.extend(quote! { #name: #property_default, });
block_default_fields.extend(quote! { #name_ident: #property_default, });
}
let block_behavior = &block.behavior;
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 {
@ -657,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
}
@ -693,11 +696,8 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let last_state_id = state_id - 1;
let mut generated = quote! {
impl BlockState {
/// Returns the highest possible state ID.
#[inline]
pub fn max_state() -> u32 {
#last_state_id
}
/// The highest possible block state ID.
pub const MAX_STATE: crate::block_state::BlockStateIntegerRepr = #last_state_id;
/// Get a property from this block state. Will be `None` if the block can't have the property.
///
@ -716,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! {};
@ -761,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
}
@ -785,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
@ -823,3 +823,13 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
generated.into()
}
/// Convert a name to a Rust identifier, replacing some Rust keywords with
/// alternatives (e.g. `type` -> `kind`).
fn name_to_ident(name: &str) -> Ident {
let ident_str = match name {
"type" => "kind",
_ => name,
};
Ident::new(ident_str, proc_macro2::Span::call_site())
}

0
azalea-block/azalea-block-macros/src/utils.rs Executable file → Normal file
View file

8
azalea-block/src/behavior.rs Executable file → Normal file
View file

@ -4,6 +4,8 @@ pub struct BlockBehavior {
pub destroy_time: f32,
pub explosion_resistance: f32,
pub requires_correct_tool_for_drops: bool,
pub force_solid: Option<bool>,
}
impl Default for BlockBehavior {
@ -14,6 +16,7 @@ impl Default for BlockBehavior {
destroy_time: 0.,
explosion_resistance: 0.,
requires_correct_tool_for_drops: false,
force_solid: None,
}
}
}
@ -52,4 +55,9 @@ impl BlockBehavior {
self.requires_correct_tool_for_drops = true;
self
}
pub fn force_solid(mut self, force_solid: bool) -> Self {
self.force_solid = Some(force_solid);
self
}
}

View file

@ -0,0 +1,178 @@
use std::{
fmt::{self, Debug},
io::{self, Cursor, Write},
};
use azalea_buf::{AzaleaRead, AzaleaReadVar, AzaleaWrite, AzaleaWriteVar, BufReadError};
use crate::BlockTrait;
/// The type that's used internally to represent a block state ID.
///
/// This should be either `u16` or `u32`. If you choose to modify it, you must
/// also change it in `azalea-block-macros/src/lib.rs`.
///
/// This does not affect protocol serialization, it just allows you to make the
/// internal type smaller if you want.
pub type BlockStateIntegerRepr = u16;
/// A representation of a state a block can be in.
///
/// For example, a stone block only has one state but each possible stair
/// rotation is a different state.
///
/// Note that this type is internally either a `u16` or `u32`, depending on
/// [`BlockStateIntegerRepr`].
#[derive(Copy, Clone, PartialEq, Eq, Default, Hash)]
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.
id: BlockStateIntegerRepr,
}
impl BlockState {
/// A shortcut for getting the air block state, since it always has an ID of
/// 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 const fn is_valid_state(state_id: BlockStateIntegerRepr) -> bool {
state_id <= Self::MAX_STATE
}
/// Returns true if the block is air. This only checks for normal air, not
/// other types like cave air.
#[inline]
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 {
type Error = ();
/// Safely converts a u32 state id to a block state.
fn try_from(state_id: u32) -> Result<Self, Self::Error> {
let state_id = state_id as BlockStateIntegerRepr;
if Self::is_valid_state(state_id) {
Ok(BlockState { id: state_id })
} else {
Err(())
}
}
}
impl TryFrom<u16> for BlockState {
type Error = ();
/// Safely converts a u16 state id to a block state.
fn try_from(state_id: u16) -> Result<Self, Self::Error> {
let state_id = state_id as BlockStateIntegerRepr;
if Self::is_valid_state(state_id) {
Ok(BlockState { id: state_id })
} else {
Err(())
}
}
}
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> {
let state_id = u32::azalea_read_var(buf)?;
Self::try_from(state_id).map_err(|_| BufReadError::UnexpectedEnumVariant {
id: state_id as i32,
})
}
}
impl AzaleaWrite for BlockState {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
u32::azalea_write_var(&(self.id as u32), buf)
}
}
impl Debug for BlockState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"BlockState(id: {}, {:?})",
self.id,
Box::<dyn BlockTrait>::from(*self)
)
}
}
impl From<BlockState> for azalea_registry::Block {
fn from(value: BlockState) -> Self {
Box::<dyn BlockTrait>::from(value).as_registry_block()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_u32() {
assert_eq!(
BlockState::try_from(0 as BlockStateIntegerRepr).unwrap(),
BlockState::AIR
);
assert!(BlockState::try_from(BlockState::MAX_STATE).is_ok());
assert!(BlockState::try_from(BlockState::MAX_STATE + 1).is_err());
}
#[test]
fn test_from_blockstate() {
let block: Box<dyn BlockTrait> = Box::<dyn BlockTrait>::from(BlockState::AIR);
assert_eq!(block.id(), "air");
let block: Box<dyn BlockTrait> =
Box::<dyn BlockTrait>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
assert_eq!(block.id(), "flowering_azalea");
}
#[test]
fn test_debug_blockstate() {
let formatted = format!(
"{:?}",
BlockState::from(azalea_registry::Block::FloweringAzalea)
);
assert!(formatted.ends_with(", FloweringAzalea)"), "{}", formatted);
let formatted = format!(
"{:?}",
BlockState::from(azalea_registry::Block::BigDripleafStem)
);
assert!(
formatted.ends_with(", BigDripleafStem { facing: North, waterlogged: false })"),
"{}",
formatted
);
}
}

View file

@ -0,0 +1,137 @@
use crate::block_state::{BlockState, BlockStateIntegerRepr};
#[derive(Clone, Debug)]
pub struct FluidState {
pub kind: FluidKind,
/// 0 = empty, 8 = full, 9 = max.
///
/// 9 is meant to be used when there's another fluid block of the same type
/// above it, but it's usually unused by this struct.
///
/// This is different from [`crate::blocks::Water::level`], which is
/// basically the opposite (0 = full, 8 = empty). You can convert between
/// the two representations with [`to_or_from_legacy_fluid_level`].
pub amount: u8,
/// Whether this fluid is at the max level and there's another fluid of the
/// same type above it.
///
/// TODO: this is currently unused (always false), make this actually get
/// set (see FlowingFluid.getFlowing)
pub falling: bool,
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub enum FluidKind {
#[default]
Empty,
Water,
Lava,
}
impl FluidState {
pub fn new_source_block(kind: FluidKind, falling: bool) -> Self {
Self {
kind,
amount: 8,
falling,
}
}
/// A floating point number in between 0 and 1 representing the height (as a
/// percentage of a full block) of the fluid.
pub fn height(&self) -> f32 {
self.amount as f32 / 9.
}
pub fn is_empty(&self) -> bool {
self.amount == 0
}
pub fn affects_flow(&self, other: &FluidState) -> bool {
other.amount == 0 || self.is_same_kind(other)
}
pub fn is_same_kind(&self, other: &FluidState) -> bool {
(other.kind == self.kind) || (self.amount == 0 && other.amount == 0)
}
}
impl Default for FluidState {
fn default() -> Self {
Self {
kind: FluidKind::Empty,
amount: 0,
falling: false,
}
}
}
impl From<BlockState> for FluidState {
fn from(state: BlockState) -> Self {
// note that 8 here might be treated as 9 in some cases if there's another fluid
// block of the same type above it
if state
.property::<crate::properties::Waterlogged>()
.unwrap_or_default()
{
return Self {
kind: FluidKind::Water,
amount: 8,
falling: false,
};
}
let registry_block = azalea_registry::Block::from(state);
match registry_block {
azalea_registry::Block::Water => {
let level = state
.property::<crate::properties::WaterLevel>()
.expect("water block should always have WaterLevel");
return Self {
kind: FluidKind::Water,
amount: to_or_from_legacy_fluid_level(level as u8),
falling: false,
};
}
azalea_registry::Block::Lava => {
let level = state
.property::<crate::properties::LavaLevel>()
.expect("lava block should always have LavaLevel");
return Self {
kind: FluidKind::Lava,
amount: to_or_from_legacy_fluid_level(level as u8),
falling: false,
};
}
azalea_registry::Block::BubbleColumn => {
return Self::new_source_block(FluidKind::Water, false);
}
_ => {}
}
Self::default()
}
}
/// Sometimes Minecraft represents fluids with 0 being empty and 8 being full,
/// and sometimes it's the opposite. You can use this function to convert
/// in between those two representations.
///
/// You usually don't need to call this yourself, see [`FluidState`].
pub fn to_or_from_legacy_fluid_level(level: u8) -> u8 {
// see FlowingFluid.getLegacyLevel
8_u8.saturating_sub(level)
}
impl From<FluidState> for BlockState {
fn from(state: FluidState) -> Self {
match state.kind {
FluidKind::Empty => BlockState::AIR,
FluidKind::Water => BlockState::from(crate::blocks::Water {
level: crate::properties::WaterLevel::from(state.amount as BlockStateIntegerRepr),
}),
FluidKind::Lava => BlockState::from(crate::blocks::Lava {
level: crate::properties::LavaLevel::from(state.amount as BlockStateIntegerRepr),
}),
}
}
}

4214
azalea-block/src/generated.rs Executable file → Normal file

File diff suppressed because it is too large Load diff

203
azalea-block/src/lib.rs Executable file → Normal file
View file

@ -1,22 +1,21 @@
#![doc = include_str!("../README.md")]
#![feature(trait_upcasting)]
mod behavior;
pub mod block_state;
pub mod fluid_state;
mod generated;
mod range;
pub use generated::{blocks, properties};
use azalea_buf::{BufReadError, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable};
pub use behavior::BlockBehavior;
use core::fmt::Debug;
pub use range::BlockStates;
use std::{
any::Any,
io::{Cursor, Write},
};
use std::any::Any;
pub trait Block: Debug + Any {
pub use behavior::BlockBehavior;
// re-exported for convenience
pub use block_state::BlockState;
pub use generated::{blocks, properties};
pub use range::BlockStates;
pub trait BlockTrait: Debug + Any {
fn behavior(&self) -> BlockBehavior;
/// Get the Minecraft ID for this block. For example `stone` or
/// `grass_block`.
@ -28,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>()
}
}
@ -39,181 +38,3 @@ pub trait Property {
fn try_from_block_state(state: BlockState) -> Option<Self::Value>;
}
/// A representation of a state a block can be in.
///
/// For example, a stone block only has one state but each possible stair
/// rotation is a different state.
#[derive(Copy, Clone, PartialEq, Eq, Default, Hash)]
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: u32,
}
impl BlockState {
pub const AIR: BlockState = BlockState { id: 0 };
#[inline]
pub fn is_valid_state(state_id: u32) -> bool {
state_id <= Self::max_state()
}
/// Returns true if the block is air. This only checks for normal air, not
/// other types like cave air.
#[inline]
pub fn is_air(&self) -> bool {
self == &Self::AIR
}
}
impl TryFrom<u32> for BlockState {
type Error = ();
/// Safely converts a state id to a block state.
fn try_from(state_id: u32) -> Result<Self, Self::Error> {
if Self::is_valid_state(state_id) {
Ok(BlockState { id: state_id })
} else {
Err(())
}
}
}
impl McBufReadable for BlockState {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let state_id = u32::var_read_from(buf)?;
Self::try_from(state_id).map_err(|_| BufReadError::UnexpectedEnumVariant {
id: state_id as i32,
})
}
}
impl McBufWritable for BlockState {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
u32::var_write_into(&self.id, buf)
}
}
impl std::fmt::Debug for BlockState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"BlockState(id: {}, {:?})",
self.id,
Box::<dyn Block>::from(*self)
)
}
}
#[derive(Clone, Debug)]
pub struct FluidState {
pub fluid: azalea_registry::Fluid,
pub height: u8,
}
impl Default for FluidState {
fn default() -> Self {
Self {
fluid: azalea_registry::Fluid::Empty,
height: 0,
}
}
}
impl From<BlockState> for FluidState {
fn from(state: BlockState) -> Self {
if state
.property::<crate::properties::Waterlogged>()
.unwrap_or_default()
{
Self {
fluid: azalea_registry::Fluid::Water,
height: 15,
}
} else {
let block = Box::<dyn Block>::from(state);
if let Some(water) = block.downcast_ref::<crate::blocks::Water>() {
Self {
fluid: azalea_registry::Fluid::Water,
height: water.level as u8,
}
} else if let Some(lava) = block.downcast_ref::<crate::blocks::Lava>() {
Self {
fluid: azalea_registry::Fluid::Lava,
height: lava.level as u8,
}
} else {
Self {
fluid: azalea_registry::Fluid::Empty,
height: 0,
}
}
}
}
}
impl From<FluidState> for BlockState {
fn from(state: FluidState) -> Self {
match state.fluid {
azalea_registry::Fluid::Empty => BlockState::AIR,
azalea_registry::Fluid::Water | azalea_registry::Fluid::FlowingWater => {
BlockState::from(crate::blocks::Water {
level: crate::properties::WaterLevel::from(state.height as u32),
})
}
azalea_registry::Fluid::Lava | azalea_registry::Fluid::FlowingLava => {
BlockState::from(crate::blocks::Lava {
level: crate::properties::LavaLevel::from(state.height as u32),
})
}
}
}
}
impl From<BlockState> for azalea_registry::Block {
fn from(value: BlockState) -> Self {
Box::<dyn Block>::from(value).as_registry_block()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_u32() {
assert_eq!(BlockState::try_from(0).unwrap(), BlockState::AIR);
assert!(BlockState::try_from(BlockState::max_state()).is_ok());
assert!(BlockState::try_from(BlockState::max_state() + 1).is_err());
}
#[test]
fn test_from_blockstate() {
let block: Box<dyn Block> = Box::<dyn Block>::from(BlockState::AIR);
assert_eq!(block.id(), "air");
let block: Box<dyn Block> =
Box::<dyn Block>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
assert_eq!(block.id(), "flowering_azalea");
}
#[test]
fn test_debug_blockstate() {
let formatted = format!(
"{:?}",
BlockState::from(azalea_registry::Block::FloweringAzalea)
);
assert!(formatted.ends_with(", FloweringAzalea)"), "{}", formatted);
let formatted = format!(
"{:?}",
BlockState::from(azalea_registry::Block::BigDripleafStem)
);
assert!(
formatted.ends_with(", BigDripleafStem { facing: North, waterlogged: false })"),
"{}",
formatted
);
}
}

View file

@ -1,20 +1,22 @@
use std::{
collections::HashSet,
collections::{HashSet, hash_set},
ops::{Add, RangeInclusive},
};
use crate::BlockState;
use azalea_registry::Block;
use crate::{BlockState, block_state::BlockStateIntegerRepr};
#[derive(Debug, Clone)]
pub struct BlockStates {
pub set: HashSet<BlockState>,
}
impl From<RangeInclusive<u32>> for BlockStates {
fn from(range: RangeInclusive<u32>) -> Self {
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 }
}
@ -22,7 +24,7 @@ impl From<RangeInclusive<u32>> for BlockStates {
impl IntoIterator for BlockStates {
type Item = BlockState;
type IntoIter = std::collections::hash_set::IntoIter<BlockState>;
type IntoIter = hash_set::IntoIter<BlockState>;
fn into_iter(self) -> Self::IntoIter {
self.set.into_iter()
@ -44,3 +46,34 @@ impl Add for BlockStates {
}
}
}
impl From<HashSet<Block>> for BlockStates {
fn from(set: HashSet<Block>) -> Self {
Self::from(&set)
}
}
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

@ -1,17 +1,19 @@
[package]
description = "A port of Mojang's Brigadier command parsing and dispatching library."
edition = "2021"
license = "MIT"
name = "azalea-brigadier"
repository = "https://github.com/azalea-rs/azalea/tree/main/azalea-brigadier"
version = "0.9.1"
description = "A port of Mojang's Brigadier command parsing and dispatching library."
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dev-dependencies]
bevy_app.workspace = true
bevy_ecs.workspace = true
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "0.9.0", optional = true }
azalea-chat = { path = "../azalea-chat", version = "0.9.0", optional = true }
parking_lot = "0.12.1"
azalea-buf = { workspace = true, optional = true }
azalea-chat = { workspace = true, optional = true }
parking_lot.workspace = true
[features]
azalea-buf = ["dep:azalea-buf", "dep:azalea-chat"]
azalea-buf = ["dep:azalea-buf", "dep:azalea-chat", "azalea-chat/azalea-buf"]

0
azalea-brigadier/README.md Executable file → Normal file
View file

4
azalea-brigadier/src/arguments/argument_type.rs Executable file → Normal file
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

@ -1,19 +1,18 @@
use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext,
exceptions::CommandSyntaxException,
errors::CommandSyntaxError,
string_reader::StringReader,
suggestion::{Suggestions, SuggestionsBuilder},
};
use super::ArgumentType;
#[derive(Default)]
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

@ -1,13 +1,12 @@
use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext,
exceptions::{BuiltInExceptions, CommandSyntaxException},
errors::{BuiltInError, CommandSyntaxError},
string_reader::StringReader,
};
use super::ArgumentType;
#[derive(Default)]
struct Double {
pub minimum: Option<f64>,
@ -15,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

@ -1,13 +1,12 @@
use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext,
exceptions::{BuiltInExceptions, CommandSyntaxException},
errors::{BuiltInError, CommandSyntaxError},
string_reader::StringReader,
};
use super::ArgumentType;
#[derive(Default)]
struct Float {
pub minimum: Option<f32>,
@ -15,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

@ -1,13 +1,12 @@
use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext,
exceptions::{BuiltInExceptions, CommandSyntaxException},
errors::{BuiltInError, CommandSyntaxError},
string_reader::StringReader,
};
use super::ArgumentType;
#[derive(Default)]
struct Integer {
pub minimum: Option<i32>,
@ -15,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

@ -1,13 +1,12 @@
use std::{any::Any, sync::Arc};
use super::ArgumentType;
use crate::{
context::CommandContext,
exceptions::{BuiltInExceptions, CommandSyntaxException},
errors::{BuiltInError, CommandSyntaxError},
string_reader::StringReader,
};
use super::ArgumentType;
#[derive(Default)]
struct Long {
pub minimum: Option<i64>,
@ -15,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))
}

0
azalea-brigadier/src/arguments/mod.rs Executable file → Normal file
View file

View file

@ -1,10 +1,7 @@
use std::{any::Any, sync::Arc};
use crate::{
context::CommandContext, exceptions::CommandSyntaxException, string_reader::StringReader,
};
use super::ArgumentType;
use crate::{context::CommandContext, errors::CommandSyntaxError, string_reader::StringReader};
pub enum StringArgument {
/// Match up until the next space.
@ -17,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()?,

44
azalea-brigadier/src/builder/argument_builder.rs Executable file → Normal file
View file

@ -1,18 +1,32 @@
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},
};
use super::{literal_argument_builder::Literal, required_argument_builder::Argument};
use std::{fmt::Debug, sync::Arc};
#[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)
}

0
azalea-brigadier/src/builder/mod.rs Executable file → Normal file
View file

View file

@ -1,36 +1,52 @@
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},
};
use std::{any::Any, fmt::Debug, sync::Arc};
/// 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> {
@ -38,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)
@ -58,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(),
}
}
}

241
azalea-brigadier/src/command_dispatcher.rs Executable file → Normal file
View file

@ -1,21 +1,23 @@
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
ptr,
rc::Rc,
sync::Arc,
};
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,
};
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
mem,
rc::Rc,
sync::Arc,
};
/// The root of the command tree. You need to make this to register commands.
///
@ -29,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),
}
}
@ -51,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());
@ -63,9 +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();
let mut errors = HashMap::<Rc<CommandNode<S>>, CommandSyntaxException>::new();
#[allow(clippy::mutable_key_type)] // this is fine because we don't mutate the key
let mut errors = HashMap::<Rc<CommandNode<S>>, CommandSyntaxError>::new();
let mut potentials: Vec<ParseResults<S>> = vec![];
let cursor = original_reader.cursor();
@ -81,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),
@ -92,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;
@ -106,23 +110,30 @@ impl<S> CommandDispatcher<S> {
1
}) {
reader.skip();
if let Some(redirect) = &child.read().redirect {
let child_context =
CommandContextBuilder::new(self, source, redirect.clone(), reader.cursor);
let parse = self
.parse_nodes(redirect, &reader, child_context)
.expect("Parsing nodes failed");
context.with_child(Rc::new(parse.context));
return Ok(ParseResults {
context,
reader: parse.reader,
exceptions: parse.exceptions,
});
} else {
let parse = self
.parse_nodes(&child, &reader, context)
.expect("Parsing nodes failed");
potentials.push(parse);
match &child.read().redirect {
Some(redirect) => {
let child_context = CommandContextBuilder::new(
self,
source,
redirect.clone(),
reader.cursor,
);
let parse = self
.parse_nodes(redirect, &reader, child_context)
.expect("Parsing nodes failed");
context.with_child(Rc::new(parse.context));
return Ok(ParseResults {
context,
reader: parse.reader,
exceptions: parse.exceptions,
});
}
_ => {
let parse = self
.parse_nodes(&child, &reader, context)
.expect("Parsing nodes failed");
potentials.push(parse);
}
}
} else {
potentials.push(ParseResults {
@ -170,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(
@ -213,96 +224,39 @@ impl<S> CommandDispatcher<S> {
pub fn find_node(&self, path: &[&str]) -> Option<Arc<RwLock<CommandNode<S>>>> {
let mut node = self.root.clone();
for name in path {
if let Some(child) = node.clone().read().child(name) {
node = child;
} else {
return None;
}
match node.clone().read().child(name) {
Some(child) => {
node = child;
}
_ => {
return None;
}
};
}
Some(node)
}
/// 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);
if let Ok(results) = results {
if !results.is_empty() {
next.extend(results.iter().map(|s| child.copy_for(s.clone())));
}
} else {
// 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(
@ -330,32 +284,35 @@ impl<S> CommandDispatcher<S> {
if node.command.is_some() {
result.push(prefix.to_owned());
}
if let Some(redirect) = &node.redirect {
let redirect = if redirect.data_ptr() == self.root.data_ptr() {
"...".to_string()
} else {
format!("-> {}", redirect.read().usage_text())
};
if prefix.is_empty() {
result.push(format!("{} {redirect}", node.usage_text()));
} else {
result.push(format!("{prefix} {redirect}"));
match &node.redirect {
Some(redirect) => {
let redirect = if ptr::eq(redirect.data_ptr(), self.root.data_ptr()) {
"...".to_string()
} else {
format!("-> {}", redirect.read().usage_text())
};
if prefix.is_empty() {
result.push(format!("{} {redirect}", node.usage_text()));
} else {
result.push(format!("{prefix} {redirect}"));
}
}
} else {
for child in node.children.values() {
let child = child.read();
self.get_all_usage_recursive(
&child,
source,
result,
if prefix.is_empty() {
child.usage_text()
} else {
format!("{prefix} {}", child.usage_text())
}
.as_str(),
restricted,
);
_ => {
for child in node.children.values() {
let child = child.read();
self.get_all_usage_recursive(
&child,
source,
result,
if prefix.is_empty() {
child.usage_text()
} else {
format!("{prefix} {}", child.usage_text())
}
.as_str(),
restricted,
);
}
}
}
}
@ -407,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())
@ -447,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
};

79
azalea-brigadier/src/context/command_context.rs Executable file → Normal file
View file

@ -1,24 +1,31 @@
use std::{
any::Any,
collections::HashMap,
fmt::{self, Debug},
rc::Rc,
sync::Arc,
};
use parking_lot::RwLock;
use super::{parsed_command_node::ParsedCommandNode, string_range::StringRange, ParsedArgument};
use super::{ParsedArgument, parsed_command_node::ParsedCommandNode, string_range::StringRange};
use crate::{
modifier::RedirectModifier,
tree::{Command, CommandNode},
};
use std::{any::Any, collections::HashMap, fmt::Debug, rc::Rc, sync::Arc};
/// 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> {
@ -39,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)
@ -58,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(),
@ -74,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
}
}

42
azalea-brigadier/src/context/command_context_builder.rs Executable file → Normal file
View file

@ -1,15 +1,21 @@
use std::{
collections::HashMap,
fmt::{self, Debug},
rc::Rc,
sync::Arc,
};
use parking_lot::RwLock;
use super::{
command_context::CommandContext, parsed_command_node::ParsedCommandNode,
string_range::StringRange, suggestion_context::SuggestionContext, ParsedArgument,
ParsedArgument, command_context::CommandContext, parsed_command_node::ParsedCommandNode,
string_range::StringRange, suggestion_context::SuggestionContext,
};
use crate::{
command_dispatcher::CommandDispatcher,
modifier::RedirectModifier,
tree::{Command, CommandNode},
};
use std::{collections::HashMap, fmt::Debug, rc::Rc, sync::Arc};
pub struct CommandContextBuilder<'a, S> {
pub arguments: HashMap<String, ParsedArgument>,
@ -63,7 +69,7 @@ impl<'a, S> CommandContextBuilder<'a, S> {
}
pub fn with_command(&mut self, command: &Command<S>) -> &Self {
self.command = command.clone();
self.command.clone_from(command);
self
}
pub fn with_child(&mut self, child: Rc<CommandContextBuilder<'a, S>>) -> &Self {
@ -80,7 +86,7 @@ impl<'a, S> CommandContextBuilder<'a, S> {
range,
});
self.range = StringRange::encompassing(&self.range, &range);
self.modifier = node.read().modifier.clone();
self.modifier.clone_from(&node.read().modifier);
self.forks = node.read().forks;
self
}
@ -106,18 +112,18 @@ impl<'a, S> CommandContextBuilder<'a, S> {
}
if self.range.end() < cursor {
if let Some(child) = &self.child {
child.find_suggestion_context(cursor)
} else if let Some(last) = self.nodes.last() {
SuggestionContext {
parent: Arc::clone(&last.node),
start_pos: last.range.end() + 1,
}
} else {
SuggestionContext {
parent: Arc::clone(&self.root),
start_pos: self.range.start(),
}
match &self.child {
Some(child) => child.find_suggestion_context(cursor),
_ => match self.nodes.last() {
Some(last) => SuggestionContext {
parent: Arc::clone(&last.node),
start_pos: last.range.end() + 1,
},
_ => SuggestionContext {
parent: Arc::clone(&self.root),
start_pos: self.range.start(),
},
},
}
} else {
let mut prev = &self.root;
@ -139,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,
}

2
azalea-brigadier/src/context/mod.rs Executable file → Normal file
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;

3
azalea-brigadier/src/context/parsed_argument.rs Executable file → Normal file
View file

@ -1,6 +1,7 @@
use super::string_range::StringRange;
use std::{any::Any, sync::Arc};
use super::string_range::StringRange;
#[derive(Clone)]
pub struct ParsedArgument {
pub range: StringRange,

3
azalea-brigadier/src/context/parsed_command_node.rs Executable file → Normal file
View file

@ -1,8 +1,9 @@
use std::sync::Arc;
use parking_lot::RwLock;
use super::string_range::StringRange;
use crate::tree::CommandNode;
use std::sync::Arc;
#[derive(Debug)]
pub struct ParsedCommandNode<S> {

0
azalea-brigadier/src/context/string_range.rs Executable file → Normal file
View file

View file

@ -1,11 +1,10 @@
use std::fmt;
use super::command_syntax_error::CommandSyntaxError;
use crate::string_reader::StringReader;
use super::command_syntax_exception::CommandSyntaxException;
#[derive(Clone, PartialEq)]
pub enum BuiltInExceptions {
pub enum BuiltInError {
DoubleTooSmall { found: f64, min: f64 },
DoubleTooBig { found: f64, max: f64 },
@ -41,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,94 +0,0 @@
use super::builtin_exceptions::BuiltInExceptions;
use std::{
cmp,
fmt::{self, Write},
};
#[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;

3
azalea-brigadier/src/lib.rs Executable file → Normal file
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;

4
azalea-brigadier/src/modifier.rs Executable file → Normal file
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;

17
azalea-brigadier/src/parse_results.rs Executable file → Normal file
View file

@ -1,17 +1,22 @@
use crate::{
context::CommandContextBuilder, exceptions::CommandSyntaxException,
string_reader::StringReader, tree::CommandNode,
use std::{
collections::HashMap,
fmt::{self, Debug},
rc::Rc,
};
use crate::{
context::CommandContextBuilder, errors::CommandSyntaxError, string_reader::StringReader,
tree::CommandNode,
};
use std::{collections::HashMap, fmt::Debug, rc::Rc};
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) {}
}

54
azalea-brigadier/src/string_reader.rs Executable file → Normal file
View file

@ -1,6 +1,7 @@
use crate::exceptions::{BuiltInExceptions, CommandSyntaxException};
use std::str::FromStr;
use crate::errors::{BuiltInError, CommandSyntaxError};
#[derive(Clone)]
pub struct StringReader {
string: String,
@ -90,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));
@ -111,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));
@ -132,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));
@ -153,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));
@ -192,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() {
@ -218,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 {
@ -230,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());
}
@ -245,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" {
@ -258,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(())

31
azalea-brigadier/src/suggestion/mod.rs Executable file → Normal file
View file

@ -1,20 +1,25 @@
mod suggestion_provider;
mod suggestions;
mod suggestions_builder;
use crate::context::StringRange;
#[cfg(feature = "azalea-buf")]
use azalea_buf::McBufWritable;
#[cfg(feature = "azalea-buf")]
use azalea_chat::FormattedText;
#[cfg(feature = "azalea-buf")]
use std::io::Write;
use std::io::{self, Write};
use std::{
cmp::Ordering,
fmt::{self, Display},
hash::Hash,
};
#[cfg(feature = "azalea-buf")]
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;
use crate::context::StringRange;
/// A suggestion given to the user for what they might want to type next.
///
/// The `M` generic is the type of the tooltip, so for example a `String` or
@ -91,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())
@ -116,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),
@ -129,19 +134,19 @@ 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 McBufWritable for Suggestion {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
self.value.to_string().write_into(buf)?;
impl AzaleaWrite for Suggestion {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
self.value.to_string().azalea_write(buf)?;
self.tooltip
.clone()
.map(FormattedText::from)
.write_into(buf)?;
.azalea_write(buf)?;
Ok(())
}
}

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;
}

42
azalea-brigadier/src/suggestion/suggestions.rs Executable file → Normal file
View file

@ -1,16 +1,16 @@
#[cfg(feature = "azalea-buf")]
use std::io::{self, Cursor, Write};
use std::{collections::HashSet, hash::Hash};
#[cfg(feature = "azalea-buf")]
use azalea_buf::{AzBuf, AzaleaRead, AzaleaReadVar, AzaleaWrite, AzaleaWriteVar, BufReadError};
#[cfg(feature = "azalea-buf")]
use azalea_chat::FormattedText;
use super::Suggestion;
use crate::context::StringRange;
#[cfg(feature = "azalea-buf")]
use crate::suggestion::SuggestionValue;
#[cfg(feature = "azalea-buf")]
use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
};
#[cfg(feature = "azalea-buf")]
use azalea_chat::FormattedText;
#[cfg(feature = "azalea-buf")]
use std::io::{Cursor, Write};
use std::{collections::HashSet, hash::Hash};
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct Suggestions {
@ -77,21 +77,21 @@ impl Suggestions {
}
#[cfg(feature = "azalea-buf")]
impl McBufReadable for Suggestions {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
#[derive(McBuf)]
impl AzaleaRead for Suggestions {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
#[derive(AzBuf)]
struct StandaloneSuggestion {
pub text: String,
pub tooltip: Option<FormattedText>,
}
let start = u32::var_read_from(buf)? as usize;
let length = u32::var_read_from(buf)? as usize;
let start = u32::azalea_read_var(buf)? as usize;
let length = u32::azalea_read_var(buf)? as usize;
let range = StringRange::between(start, start + length);
// the range of a Suggestion depends on the Suggestions containing it,
// so we can't just `impl McBufReadable for Suggestion`
let mut suggestions = Vec::<StandaloneSuggestion>::read_from(buf)?
// so we can't just `impl AzaleaRead for Suggestion`
let mut suggestions = Vec::<StandaloneSuggestion>::azalea_read(buf)?
.into_iter()
.map(|s| Suggestion {
value: SuggestionValue::Text(s.text),
@ -106,11 +106,11 @@ impl McBufReadable for Suggestions {
}
#[cfg(feature = "azalea-buf")]
impl McBufWritable for Suggestions {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
(self.range.start() as u32).var_write_into(buf)?;
(self.range.length() as u32).var_write_into(buf)?;
self.suggestions.write_into(buf)?;
impl AzaleaWrite for Suggestions {
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)?;
Ok(())
}
}

3
azalea-brigadier/src/suggestion/suggestions_builder.rs Executable file → Normal file
View file

@ -1,8 +1,7 @@
use std::collections::HashSet;
use crate::context::StringRange;
use super::{Suggestion, SuggestionValue, Suggestions};
use crate::context::StringRange;
#[derive(PartialEq, Debug)]
pub struct SuggestionsBuilder {

67
azalea-brigadier/src/tree/mod.rs Executable file → Normal file
View file

@ -1,3 +1,11 @@
use std::{
collections::{BTreeMap, HashMap},
fmt::{self, Debug},
hash::{Hash, Hasher},
ptr,
sync::Arc,
};
use parking_lot::RwLock;
use crate::{
@ -6,25 +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},
};
use std::{
collections::{BTreeMap, HashMap},
fmt::Debug,
hash::Hash,
ptr,
sync::Arc,
};
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>>>>,
@ -65,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"),
@ -148,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();
@ -175,7 +177,7 @@ impl<S> CommandNode<S> {
return Ok(());
}
Err(BuiltInExceptions::LiteralIncorrect {
Err(BuiltInError::LiteralIncorrect {
expected: literal.value.clone(),
}
.create_with_context(reader))
@ -213,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 {
@ -230,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)
@ -268,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);
@ -291,18 +291,27 @@ impl<S> PartialEq for CommandNode<S> {
}
}
if let Some(selfexecutes) = &self.command {
// idk how to do this better since we can't compare `dyn Fn`s
if let Some(otherexecutes) = &other.command {
#[allow(ambiguous_wide_pointer_comparisons)]
if !Arc::ptr_eq(selfexecutes, otherexecutes) {
match &self.command {
Some(selfexecutes) => {
// idk how to do this better since we can't compare `dyn Fn`s
match &other.command {
Some(otherexecutes) =>
{
#[allow(ambiguous_wide_pointer_comparisons)]
if !Arc::ptr_eq(selfexecutes, otherexecutes) {
return false;
}
}
_ => {
return false;
}
}
}
_ => {
if other.command.is_some() {
return false;
}
} else {
return false;
}
} else if other.command.is_some() {
return false;
}
true
}

View file

View file

View file

View file

View file

View file

View file

@ -0,0 +1,189 @@
use std::{mem, ops::Deref, sync::Arc};
use azalea_brigadier::prelude::*;
use bevy_app::App;
use bevy_ecs::{prelude::*, system::RunSystemOnce};
use parking_lot::Mutex;
#[test]
fn bevy_app() {
let mut app = App::new();
// Initialize the dispatcher using FromWorld
app.init_resource::<DispatchStorage>();
// Process commands from bevy
if let Err(err) = app
.world_mut()
.run_system_once(DispatchStorage::bevy_process_commands)
{
panic!("Failed to process commands: {err}");
}
// Verify spawned entities exist after processing commands
if let Err(err) = app
.world_mut()
.run_system_once(DispatchStorage::verify_spawned_entities)
{
panic!("Failed to verify spawned entities: {err}");
}
}
#[derive(Resource)]
struct DispatchStorage {
/// The [`CommandDispatcher`].
///
/// Processes incoming commands.
dispatch: CommandDispatcher<WorldAccessor>,
/// The world accessor.
///
/// Allows the dispatcher to query the [`World`].
world: WorldAccessor,
}
/// Implement [`FromWorld`] to initialize the dispatcher.
///
/// Allows the dispatcher to query the [`World`]
/// for generating commands on startup.
impl FromWorld for DispatchStorage {
fn from_world(_: &mut World) -> Self {
let mut dispatch = CommandDispatcher::new();
// Register dispatcher commands
{
// Register the "spawn_entity" command
dispatch
.register(literal("spawn_entity").executes(DispatchStorage::command_spawn_entity));
// Register the "spawn_entity_num" command
dispatch.register(literal("spawn_entity_num").then(
argument("entities", integer()).executes(DispatchStorage::command_spawn_entity_num),
));
}
Self {
dispatch,
world: WorldAccessor::empty(),
}
}
}
impl DispatchStorage {
/// A bevy system called to process commands.
fn bevy_process_commands(world: &mut World) {
world.resource_scope::<Self, _>(|bevy_world, mut storage| {
// NOTE: Initial swap to own bevy's `World`
//
// This is important, otherwise the dispatcher
// will only be able to access it's own empty `World`.
storage.world.swap(bevy_world);
let source = storage.world.clone();
// Test "spawn_entity"
{
println!("Testing 'spawn_entity' command");
let result = storage.dispatch.execute("spawn_entity", source.clone());
// Ensure the command was successful
assert_eq!(result, Ok(0));
// Query the World for the spawned entity
let mut world = source.lock();
let mut query = world.query_filtered::<(), With<SpawnedEntity>>();
// Ensure only one entity was spawned
let count = query.iter(&world).count();
println!("Spawned entities: {count}");
assert_eq!(count, 1);
}
// Test "spawn_entity_num"
{
println!("Testing 'spawn_entity_num' command");
let result = storage
.dispatch
.execute("spawn_entity_num 3", source.clone());
// Ensure the command was successful
assert_eq!(result, Ok(0));
// Query the World for spawned entities
let mut world = source.lock();
let mut query = world.query_filtered::<(), With<SpawnedEntity>>();
// Ensure three additional entities were spawned
let count = query.iter(&world).count();
println!("Spawned entities: {count}");
assert_eq!(count, 4);
}
// NOTE: Second swap to give bevy's `World` back
//
// It's even more important to give the `World` back
// after commands are executed, otherwise your app
// will be stuck with an empty `World`.
storage.world.swap(bevy_world);
});
}
/// A command called from the dispatcher.
///
/// Spawns an entity with the [`SpawnedEntity`] component.
fn command_spawn_entity(context: &CommandContext<WorldAccessor>) -> i32 {
context.source.lock().spawn(SpawnedEntity);
0
}
/// A command called from the dispatcher.
///
/// Spawns a number of entities with the [`SpawnedEntity`] component.
fn command_spawn_entity_num(context: &CommandContext<WorldAccessor>) -> i32 {
let num = get_integer(context, "entities").unwrap();
for _ in 0..num {
context.source.lock().spawn(SpawnedEntity);
}
0
}
/// A bevy system called to verify four total entities was spawned.
fn verify_spawned_entities(query: Query<(), With<SpawnedEntity>>) {
assert_eq!(query.iter().count(), 4);
}
}
/// A wrapper around a [`World`] that allows for
/// access from inside a [`CommandDispatcher`].
#[derive(Clone)]
struct WorldAccessor {
world: Arc<Mutex<World>>,
}
impl WorldAccessor {
/// Create a new empty [`WorldAccessor`].
fn empty() -> Self {
Self {
world: Arc::new(Mutex::new(World::new())),
}
}
/// Swap the internal [`World`] with the given one.
fn swap(&mut self, world: &mut World) {
mem::swap(&mut *self.lock(), world);
}
}
/// A marker [`Component`] used to test spawning entities from the dispatcher.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Component)]
struct SpawnedEntity;
/// Implemented for convenience.
impl Deref for WorldAccessor {
type Target = Arc<Mutex<World>>;
fn deref(&self) -> &Self::Target {
&self.world
}
}

12
azalea-brigadier/tests/builder/argument_builder_test.rs Executable file → Normal file
View file

@ -10,11 +10,13 @@ fn test_arguments() {
let builder = builder.then(argument.clone());
assert_eq!(builder.arguments().children.len(), 1);
let built_argument = Rc::new(argument.build());
assert!(builder
.arguments()
.children
.values()
.any(|e| *e.read() == *built_argument));
assert!(
builder
.arguments()
.children
.values()
.any(|e| *e.read() == *built_argument)
);
}
// @Test

View file

View file

54
azalea-brigadier/tests/command_dispatcher_test.rs Executable file → Normal file
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

0
azalea-brigadier/tests/command_suggestions_test.rs Executable file → Normal file
View file

0
azalea-brigadier/tests/context/command_context_test.rs Executable file → Normal file
View file

0
azalea-brigadier/tests/context/parsed_argument_test.rs Executable file → Normal file
View file

52
azalea-brigadier/tests/string_reader_test.rs Executable file → Normal file
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));
}
}

0
azalea-brigadier/tests/suggestion/suggestion_test.rs Executable file → Normal file
View file

View file

2
azalea-brigadier/tests/suggestion/suggestions_test.rs Executable file → Normal file
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

View file

View file

0
azalea-brigadier/tests/tree/root_command_node_test.rs Executable file → Normal file
View file

View file

@ -1,21 +1,19 @@
[package]
description = "Serialize and deserialize buffers from Minecraft."
edition = "2021"
license = "MIT"
name = "azalea-buf"
repository = "https://github.com/azalea-rs/azalea/tree/main/azalea-buf"
version = "0.9.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
description = "Serialize and deserialize buffers from Minecraft."
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
simdnbt = { version = "0.4", git = "https://github.com/azalea-rs/simdnbt" }
azalea-buf-macros = { path = "./azalea-buf-macros", version = "0.9.0" }
byteorder = "^1.5.0"
tracing = "0.1.40"
serde_json = { version = "^1.0", optional = true }
thiserror = "1.0.57"
uuid = "^1.7.0"
azalea-buf-macros.workspace = true
byteorder.workspace = true
serde_json = { workspace = true, optional = true }
simdnbt.workspace = true
thiserror.workspace = true
tracing.workspace = true
uuid.workspace = true
[features]
serde_json = ["dep:serde_json"]

0
azalea-buf/README.md Executable file → Normal file
View file

View file

@ -1,16 +1,15 @@
[package]
description = "#[derive(McBuf)]"
edition = "2021"
license = "MIT"
name = "azalea-buf-macros"
repository = "https://github.com/azalea-rs/azalea/tree/main/azalea-buf"
version = "0.9.1"
description = "#[derive(AzBuf)]"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
proc-macro = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "^1.0.78"
quote = "^1.0.35"
syn = { version = "^2.0.49", features = ["extra-traits"] }
proc-macro2.workspace = true
quote.workspace = true
syn = { workspace = true, features = ["extra-traits"] }

0
azalea-buf/azalea-buf-macros/README.md Executable file → Normal file
View file

22
azalea-buf/azalea-buf-macros/src/lib.rs Executable file → Normal file
View file

@ -3,28 +3,28 @@ mod write;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use syn::{DeriveInput, parse_macro_input};
#[proc_macro_derive(McBufReadable, attributes(var))]
pub fn derive_mcbufreadable(input: TokenStream) -> TokenStream {
#[proc_macro_derive(AzaleaRead, attributes(var))]
pub fn derive_azalearead(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
read::create_impl_mcbufreadable(&ident, &data).into()
read::create_impl_azalearead(&ident, &data).into()
}
#[proc_macro_derive(McBufWritable, attributes(var))]
pub fn derive_mcbufwritable(input: TokenStream) -> TokenStream {
#[proc_macro_derive(AzaleaWrite, attributes(var))]
pub fn derive_azaleawrite(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
write::create_impl_mcbufwritable(&ident, &data).into()
write::create_impl_azaleawrite(&ident, &data).into()
}
#[proc_macro_derive(McBuf, attributes(var))]
pub fn derive_mcbuf(input: TokenStream) -> TokenStream {
#[proc_macro_derive(AzBuf, attributes(var, limit))]
pub fn derive_azbuf(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
let writable = write::create_impl_mcbufwritable(&ident, &data);
let readable = read::create_impl_mcbufreadable(&ident, &data);
let writable = write::create_impl_azaleawrite(&ident, &data);
let readable = read::create_impl_azalearead(&ident, &data);
quote! {
#writable
#readable

View file

@ -1,61 +1,46 @@
use quote::{quote, ToTokens};
use syn::{punctuated::Punctuated, token::Comma, Data, Field, FieldsNamed, Ident};
use quote::{ToTokens, quote};
use syn::{Data, Field, FieldsNamed, Ident, punctuated::Punctuated, token::Comma};
fn read_named_fields(
named: &Punctuated<Field, Comma>,
) -> (Vec<proc_macro2::TokenStream>, Vec<&Option<Ident>>) {
let read_fields = named
.iter()
.map(|f| {
let field_name = &f.ident;
let field_type = &f.ty;
// do a different buf.write_* for each field depending on the type
// if it's a string, use buf.write_string
match field_type {
syn::Type::Path(_) | syn::Type::Array(_) => {
if f.attrs.iter().any(|a| a.path().is_ident("var")) {
quote! {
let #field_name = azalea_buf::McBufVarReadable::var_read_from(buf)?;
}
} else {
quote! {
let #field_name = azalea_buf::McBufReadable::read_from(buf)?;
}
pub fn create_impl_azalearead(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
match data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => match fields {
syn::Fields::Named(FieldsNamed { named, .. }) => {
let (read_fields, read_field_names) = read_named_fields(named);
quote! {
impl azalea_buf::AzaleaRead for #ident {
fn azalea_read(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
#(#read_fields)*
Ok(Self {
#(#read_field_names: #read_field_names),*
})
}
}
_ => panic!(
"Error reading field {}: {}",
field_name.clone().unwrap(),
field_type.to_token_stream()
),
}
})
.collect::<Vec<_>>();
let read_field_names = named.iter().map(|f| &f.ident).collect::<Vec<_>>();
(read_fields, read_field_names)
}
pub fn create_impl_mcbufreadable(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
match data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => {
let syn::Fields::Named(FieldsNamed { named, .. }) = fields else {
panic!("#[derive(McBuf)] can only be used on structs with named fields")
};
let (read_fields, read_field_names) = read_named_fields(named);
quote! {
impl azalea_buf::McBufReadable for #ident {
fn read_from(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
#(#read_fields)*
Ok(#ident {
#(#read_field_names: #read_field_names),*
})
}
}
syn::Fields::Unit => {
quote! {
impl azalea_buf::AzaleaRead for #ident {
fn azalea_read(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
Ok(Self)
}
}
}
}
}
syn::Fields::Unnamed(fields) => {
let read_fields = read_unnamed_fields(&fields.unnamed);
quote! {
impl azalea_buf::AzaleaRead for #ident {
fn azalea_read(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
Ok(Self(
#(#read_fields),*
))
}
}
}
}
},
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
let mut match_contents = quote!();
let mut variant_discrim: u32 = 0;
@ -98,13 +83,34 @@ pub fn create_impl_mcbufreadable(ident: &Ident, data: &Data) -> proc_macro2::Tok
syn::Fields::Unnamed(fields) => {
let mut reader_code = quote! {};
for f in &fields.unnamed {
if f.attrs.iter().any(|attr| attr.path().is_ident("var")) {
let is_variable_length =
f.attrs.iter().any(|a| a.path().is_ident("var"));
let limit =
f.attrs
.iter()
.find(|a| a.path().is_ident("limit"))
.map(|a| {
a.parse_args::<syn::LitInt>()
.unwrap()
.base10_parse::<usize>()
.unwrap()
});
if is_variable_length && limit.is_some() {
panic!("Fields cannot have both var and limit attributes");
}
if is_variable_length {
reader_code.extend(quote! {
Self::#variant_name(azalea_buf::McBufVarReadable::var_read_from(buf)?),
Self::#variant_name(azalea_buf::AzaleaReadVar::azalea_read_var(buf)?),
});
} else if let Some(limit) = limit {
reader_code.extend(quote! {
Self::#variant_name(azalea_buf::AzaleaReadLimited::azalea_read_limited(buf, #limit)?),
});
} else {
reader_code.extend(quote! {
Self::#variant_name(azalea_buf::McBufReadable::read_from(buf)?),
Self::#variant_name(azalea_buf::AzaleaRead::azalea_read(buf)?),
});
}
}
@ -129,15 +135,15 @@ pub fn create_impl_mcbufreadable(ident: &Ident, data: &Data) -> proc_macro2::Tok
let first_reader = first_reader.expect("There should be at least one variant");
quote! {
impl azalea_buf::McBufReadable for #ident {
fn read_from(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
let id = azalea_buf::McBufVarReadable::var_read_from(buf)?;
Self::read_from_id(buf, id)
impl azalea_buf::AzaleaRead for #ident {
fn azalea_read(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
let id = azalea_buf::AzaleaReadVar::azalea_read_var(buf)?;
Self::azalea_read_id(buf, id)
}
}
impl #ident {
pub fn read_from_id(buf: &mut std::io::Cursor<&[u8]>, id: u32) -> Result<Self, azalea_buf::BufReadError> {
pub fn azalea_read_id(buf: &mut std::io::Cursor<&[u8]>, id: u32) -> Result<Self, azalea_buf::BufReadError> {
match id {
#match_contents
// you'd THINK this throws an error, but mojang decided to make it default for some reason
@ -147,6 +153,81 @@ pub fn create_impl_mcbufreadable(ident: &Ident, data: &Data) -> proc_macro2::Tok
}
}
}
_ => panic!("#[derive(McBuf)] can only be used on structs"),
_ => panic!("#[derive(AzBuf)] can only be used on structs"),
}
}
fn read_named_fields(
named: &Punctuated<Field, Comma>,
) -> (Vec<proc_macro2::TokenStream>, Vec<&Option<Ident>>) {
let read_fields = named
.iter()
.map(|f| {
let field_name = &f.ident;
let reader_call = get_reader_call(f);
quote! { let #field_name = #reader_call; }
})
.collect::<Vec<_>>();
let read_field_names = named.iter().map(|f| &f.ident).collect::<Vec<_>>();
(read_fields, read_field_names)
}
fn read_unnamed_fields(unnamed: &Punctuated<Field, Comma>) -> Vec<proc_macro2::TokenStream> {
unnamed
.iter()
.map(|f| {
let reader_call = get_reader_call(f);
quote! { #reader_call }
})
.collect::<Vec<_>>()
}
fn get_reader_call(f: &Field) -> proc_macro2::TokenStream {
let is_variable_length = f
.attrs
.iter()
.any(|a: &syn::Attribute| a.path().is_ident("var"));
let limit = f
.attrs
.iter()
.find(|a| a.path().is_ident("limit"))
.map(|a| {
a.parse_args::<syn::LitInt>()
.unwrap()
.base10_parse::<usize>()
.unwrap()
});
if is_variable_length && limit.is_some() {
panic!("Fields cannot have both var and limit attributes");
}
let field_type = &f.ty;
// do a different buf.write_* for each field depending on the type
// if it's a string, use buf.write_string
match field_type {
syn::Type::Path(_) | syn::Type::Array(_) => {
if is_variable_length {
quote! {
azalea_buf::AzaleaReadVar::azalea_read_var(buf)?
}
} else if let Some(limit) = limit {
quote! {
azalea_buf::AzaleaReadLimited::azalea_read_limited(buf, #limit)?
}
} else {
quote! {
azalea_buf::AzaleaRead::azalea_read(buf)?
}
}
}
_ => panic!(
"Error reading field {:?}: {}",
f.ident.clone(),
field_type.to_token_stream()
),
}
}

View file

@ -1,61 +1,45 @@
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::{punctuated::Punctuated, token::Comma, Data, Field, FieldsNamed, Ident};
use quote::{ToTokens, quote};
use syn::{Data, Field, FieldsNamed, Ident, punctuated::Punctuated, token::Comma};
fn write_named_fields(
named: &Punctuated<Field, Comma>,
ident_name: Option<&Ident>,
) -> proc_macro2::TokenStream {
let write_fields = named.iter().map(|f| {
let field_name = &f.ident;
let field_type = &f.ty;
let ident_dot_field = match ident_name {
Some(ident) => quote! { &#ident.#field_name },
None => quote! { #field_name },
};
// do a different buf.write_* for each field depending on the type
// if it's a string, use buf.write_string
match field_type {
syn::Type::Path(_) | syn::Type::Array(_) => {
if f.attrs.iter().any(|attr| attr.path().is_ident("var")) {
quote! {
azalea_buf::McBufVarWritable::var_write_into(#ident_dot_field, buf)?;
}
} else {
quote! {
azalea_buf::McBufWritable::write_into(#ident_dot_field, buf)?;
}
}
}
_ => panic!(
"Error writing field {}: {}",
field_name.clone().unwrap(),
field_type.to_token_stream()
),
}
});
quote! { #(#write_fields)* }
}
pub fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
pub fn create_impl_azaleawrite(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
match data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => {
let syn::Fields::Named(FieldsNamed { named, .. }) = fields else {
panic!("#[derive(McBuf)] can only be used on structs with named fields")
};
syn::Data::Struct(syn::DataStruct { fields, .. }) => match fields {
syn::Fields::Named(FieldsNamed { named, .. }) => {
let write_fields =
write_named_fields(named, Some(&Ident::new("self", Span::call_site())));
let write_fields =
write_named_fields(named, Some(&Ident::new("self", Span::call_site())));
quote! {
impl azalea_buf::McBufWritable for #ident {
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
#write_fields
Ok(())
quote! {
impl azalea_buf::AzaleaWrite for #ident {
fn azalea_write(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
#write_fields
Ok(())
}
}
}
}
}
syn::Fields::Unit => {
quote! {
impl azalea_buf::AzaleaWrite for #ident {
fn azalea_write(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
Ok(())
}
}
}
}
syn::Fields::Unnamed(fields) => {
let write_fields = write_unnamed_fields(&fields.unnamed);
quote! {
impl azalea_buf::AzaleaWrite for #ident {
fn azalea_write(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
#write_fields
Ok(())
}
}
}
}
},
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
// remember whether it's a data variant so we can do an optimization later
let mut is_data_enum = false;
@ -93,7 +77,7 @@ pub fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::Tok
// the variant number that we're going to write
let write_the_variant = quote! {
azalea_buf::McBufVarWritable::var_write_into(&#variant_discrim, buf)?;
azalea_buf::AzaleaWriteVar::azalea_write_var(&#variant_discrim, buf)?;
};
match &variant.fields {
syn::Fields::Named(f) => {
@ -135,11 +119,11 @@ pub fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::Tok
params_code.extend(quote! { #param_ident, });
if f.attrs.iter().any(|attr| attr.path().is_ident("var")) {
writers_code.extend(quote! {
azalea_buf::McBufVarWritable::var_write_into(#param_ident, buf)?;
azalea_buf::AzaleaWriteVar::azalea_write_var(#param_ident, buf)?;
});
} else {
writers_code.extend(quote! {
azalea_buf::McBufWritable::write_into(#param_ident, buf)?;
azalea_buf::AzaleaWrite::azalea_write(#param_ident, buf)?;
});
}
}
@ -151,7 +135,7 @@ pub fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::Tok
});
match_arms_without_id.extend(quote! {
Self::#variant_name(data) => {
azalea_buf::McBufWritable::write_into(data, buf)?;
azalea_buf::AzaleaWrite::azalea_write(data, buf)?;
}
});
}
@ -159,8 +143,8 @@ pub fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::Tok
}
if is_data_enum {
quote! {
impl azalea_buf::McBufWritable for #ident {
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
impl azalea_buf::AzaleaWrite for #ident {
fn azalea_write(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
match self {
#match_arms
}
@ -179,14 +163,67 @@ pub fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::Tok
} else {
// optimization: if it doesn't have data we can just do `as u32`
quote! {
impl azalea_buf::McBufWritable for #ident {
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
azalea_buf::McBufVarWritable::var_write_into(&(*self as u32), buf)
impl azalea_buf::AzaleaWrite for #ident {
fn azalea_write(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
azalea_buf::AzaleaWriteVar::azalea_write_var(&(*self as u32), buf)
}
}
}
}
}
_ => panic!("#[derive(McBuf)] can only be used on structs"),
_ => panic!("#[derive(AzBuf)] can only be used on structs"),
}
}
fn write_named_fields(
named: &Punctuated<Field, Comma>,
ident_name: Option<&Ident>,
) -> proc_macro2::TokenStream {
let write_fields = named.iter().map(|f| {
let field_name = &f.ident;
let ident_dot_field = match ident_name {
Some(ident) => quote! { &#ident.#field_name },
None => quote! { #field_name },
};
make_write_call(f, ident_dot_field)
});
quote! { #(#write_fields)* }
}
fn write_unnamed_fields(named: &Punctuated<Field, Comma>) -> proc_macro2::TokenStream {
let write_fields = named.iter().enumerate().map(|(i, f)| {
let i_literal = syn::Index::from(i);
let ident_dot_field = quote! { &self.#i_literal };
make_write_call(f, ident_dot_field)
});
quote! { #(#write_fields)* }
}
fn make_write_call(
f: &Field,
ident_dot_field: proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let field_type = &f.ty;
// do a different buf.write_* for each field depending on the type
// if it's a string, use buf.write_string
match field_type {
syn::Type::Path(_) | syn::Type::Array(_) => {
if f.attrs.iter().any(|attr| attr.path().is_ident("var")) {
quote! {
azalea_buf::AzaleaWriteVar::azalea_write_var(#ident_dot_field, buf)?;
}
} else {
quote! {
azalea_buf::AzaleaWrite::azalea_write(#ident_dot_field, buf)?;
}
}
}
_ => panic!(
"Error writing field {:?}: {}",
f.ident,
field_type.to_token_stream()
),
}
}

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