mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 23:44:38 +00:00
Compare commits
398 commits
azalea-blo
...
main
Author | SHA1 | Date | |
---|---|---|---|
37b124a2b6 | |||
e7bf124ed5 | |||
9719d00526 | |||
302752860c | |||
45f89b48e4 | |||
6984a2b9e6 | |||
a5c67d2eee | |||
004741b781 | |||
63348dbbea | |||
|
df9d776ff8 |
||
|
ebc2e0c067 |
||
a060b73915 | |||
5564d475a2 | |||
2064450763 | |||
7b8dc189f7 | |||
537ec510f0 | |||
f12589ab80 | |||
08c409d048 | |||
f9e4b65713 | |||
|
af1ef93100 |
||
844697c04c | |||
ffbe7a3e42 | |||
319d144995 | |||
f82cf7fa85 | |||
fd9bf16871 | |||
713dae7110 | |||
8f70f191bb | |||
318d95491c | |||
57bdb88272 | |||
6bbb3ec5eb | |||
1fa1520fea | |||
f64229fcc9 | |||
a06b79a6c1 | |||
1a983beec1 | |||
5e81d85d7e | |||
a2606569bb | |||
89ddd5e85f | |||
df092f25ec | |||
1b348ceeff | |||
067ec06f26 | |||
ab05e7bdae | |||
9b0bd29db4 | |||
2a6ac0764f | |||
e4ead93f19 | |||
086f979a28 | |||
40bcb62a77 | |||
4a4de81961 | |||
45d7371274 | |||
3087b0c996 | |||
338f931c51 | |||
874f051810 | |||
be81877137 | |||
93a96786a8 | |||
123c15a293 | |||
bbfca34133 | |||
f5f50b85e5 | |||
f311ac27d4 | |||
415c0d873e | |||
cc3e64a315 | |||
61443fa481 | |||
1edb9d3448 | |||
f3a5e91a8c | |||
7517a207db | |||
abf995a702 | |||
04dd6dd0a4 | |||
5c0e5b1eb3 | |||
cee64cece3 | |||
c5ddae58a1 | |||
2c5f293210 | |||
5adf67fe04 | |||
0569862a1b | |||
d7cd305059 | |||
8da179b221 | |||
3d121722d7 | |||
99659bd9a3 | |||
1d3a7c969f | |||
d028d7c3e9 | |||
b103e6fdc0 | |||
0a7648ce48 | |||
f27c87b291 | |||
e37524899e | |||
ae4b1e85e6 | |||
a64c650504 | |||
cfdd8e690f | |||
a5e7ff771d | |||
da73b4316d | |||
3d340f585a | |||
bacb5c0199 | |||
acd52756be | |||
4bb317a7e1 | |||
|
9e34a2660d |
||
fbcaa6eca9 | |||
|
b35904a0b5 |
||
6a5a88700c | |||
e1d3b902ba | |||
e9b3128103 | |||
1493c06de5 | |||
5d799be9db | |||
ad4eaa174c | |||
e0d3352a90 | |||
a8e76a0bff | |||
2b7be768f2 | |||
8dff973d26 | |||
4a1fdf0121 | |||
aa0256da10 | |||
e9452032bf | |||
d58a1c4fa0 | |||
685aeff13f | |||
7b442368da | |||
f7c9419045 | |||
af3affb467 | |||
68f657310b | |||
7bbb617bd8 | |||
|
9a40b65bc1 |
||
52e34de95c | |||
50c8e6bc5b | |||
5666918519 | |||
b3f65f9d4b | |||
1d3f659c1d | |||
881055e587 | |||
|
11a74f215e |
||
4a7d21425c | |||
b3af8d73fa | |||
65c9f555b0 | |||
c84b3161ae | |||
adef9bf37b | |||
89bc5ca91e | |||
54062c82fd | |||
0fc0fe41d4 | |||
8012fb90b5 | |||
c7d53d6532 | |||
8045b4eda2 | |||
ae3722d72c | |||
fefc5db09a | |||
b7bc08e352 | |||
|
6c1b144970 |
||
77f9d929b6 | |||
e86087366f | |||
ad45cf5431 | |||
43d7c428e3 | |||
|
3f60bdadac |
||
1989f4ec97 | |||
2aa046c4b5 | |||
6a83a6fa38 | |||
b828bc2b12 | |||
66174fc7d4 | |||
a9820dfd79 | |||
1a0c4e2de9 | |||
fd27ca3bec | |||
960b840536 | |||
e2945b90a9 | |||
ed4d5937a7 | |||
31b143f821 | |||
3112dc3ea0 | |||
1be8d8638f | |||
913d6ac8c5 | |||
7dcc86e266 | |||
adf8a604c4 | |||
efc28db6cf | |||
f250978cdd | |||
5fd57fd630 | |||
e99ae608b6 | |||
1fd0292590 | |||
09d515c8cd | |||
5272db8bb4 | |||
635bed9fcb | |||
097ebb3705 | |||
55ed46d081 | |||
43ebbee94a | |||
d0b459e827 | |||
4e5c551d65 | |||
02de98240f | |||
37cde3ad32 | |||
|
ef357fdf36 |
||
8af265e48b | |||
4c53498f07 | |||
|
b25474ad66 |
||
656162d218 | |||
113222a3a2 | |||
aa2039c868 | |||
5c7332b469 | |||
13d5cbed1f | |||
75efbc83fd | |||
ca2e0b3922 | |||
|
b0bd992adc |
||
a95408cbcc | |||
5f5616145b | |||
d6a4d91347 | |||
|
aeff08f5fa |
||
7981230904 | |||
95bdb362ba | |||
f8140e4188 | |||
713b312167 | |||
249fa55a53 | |||
65fe072151 | |||
7a192acc99 | |||
e9a5df2e87 | |||
42c79043cf | |||
28a17f3ed2 | |||
cf66c4be10 | |||
c9022e8f67 | |||
72d87349fe | |||
7d6cacc79b | |||
cfe6bea25a | |||
9e061be903 | |||
172e0ce079 | |||
b9767424f3 | |||
6a5ab34a2d | |||
2be4f0f2b6 | |||
21acf4c846 | |||
dd557c8f29 | |||
|
e21e1b97bf |
||
f8130c3c92 | |||
34f53baf85 | |||
bdd2fc91e1 | |||
6fdf5fce49 | |||
444993b609 | |||
74b52a1fc1 | |||
27945c8870 | |||
f5f15362f2 | |||
5fdea4c0b7 | |||
24babbdbe5 | |||
|
833f306e8b |
||
63b1036a96 | |||
|
c285fadd34 |
||
022771b71b | |||
228489dded | |||
4fb6b07746 | |||
02f50c2dc5 | |||
6020a2297f | |||
|
858bec2081 |
||
8d110a9f7c | |||
b08d3d55d7 | |||
cdb68dfb70 | |||
479a7b8927 | |||
8d71577da8 | |||
4562967b61 | |||
87c34e1c33 | |||
4aa5010ea2 | |||
5bdb07b314 | |||
6c681adc4d | |||
6d8d937d47 | |||
befcec9b3a | |||
ef25e77e52 | |||
67c053638c | |||
ed00a5b8f1 | |||
fe423416b4 | |||
b6ddde99ea | |||
f2d8d4211b | |||
b0b57aa7d4 | |||
53fca5faf4 | |||
900a4234e5 | |||
aef29daad9 | |||
2dcfbe96c3 | |||
a86d011d4a | |||
5721eaf193 | |||
862dec529b | |||
a1435b3b95 | |||
093c99a071 | |||
608ccb8e54 | |||
a5cb21f039 | |||
e03101a7b7 | |||
31d0ac7568 | |||
55d1ae2014 | |||
|
9e98ab373f |
||
3ce7c7cc50 | |||
|
9ef53fcf2b |
||
|
0d16f01571 |
||
615d8f9d2a | |||
ebaf5128fb | |||
5693191b57 | |||
04036b6e4a | |||
6ccd44e28d | |||
185ed84dbb | |||
3c3952bb0b | |||
33e1a1326a | |||
344834c724 | |||
adb56b7eb2 | |||
3c83e5b24a | |||
e11a902fba | |||
2f1fe5f9f6 | |||
8f0d0d9280 | |||
04eaa5c3d0 | |||
0ee9ed50e3 | |||
d67aa07c13 | |||
a599b5614e | |||
f03e0c2235 | |||
de5a53ce08 | |||
958848e8ed | |||
30cbeecdfe | |||
8772661772 | |||
1609b90a93 | |||
fa132b61fc | |||
2c37ade959 | |||
63fef9d94f | |||
a477a84c6e | |||
3b1fe490b0 | |||
478fe722f5 | |||
641b99c7af | |||
98e5efb577 | |||
831e8c167f | |||
ad30950f85 | |||
7098375ecf | |||
527333f2b7 | |||
78e5a65317 | |||
c7cb73d22b | |||
5fbad9bbb3 | |||
e268c49291 | |||
|
1f06a1540f |
||
|
e9136c9cbb |
||
23932003d9 | |||
f4a1869a7d | |||
097a620de1 | |||
|
2feef49471 |
||
|
07109964ad |
||
39f4d68e1f | |||
|
6379035b85 |
||
241c7527ce | |||
|
b71a7af53a |
||
|
ea5a1c1ec1 |
||
|
944fe5c6f8 |
||
f364ad6b21 | |||
ae5c0ea8e5 | |||
c36201cc89 | |||
|
08958c2278 |
||
139d77d3c2 | |||
0817382098 | |||
|
dfdc3144b6 |
||
|
e443c5d76e |
||
|
a8125cd198 |
||
|
3cf17cb896 |
||
|
0902edb244 |
||
|
09cdc22b86 |
||
|
0774888a77 |
||
6b0fe5bf63 | |||
b762575db6 | |||
a3cf78ebec | |||
|
40e4096d24 |
||
abc7b43b8c | |||
|
5e99c2218d |
||
|
cd2f298a62 |
||
|
5535877a4b |
||
|
58d1485733 |
||
2992fc6b47 | |||
d5a281bf15 | |||
e485cf5501 | |||
74831abbe4 | |||
73091d8f93 | |||
dec544a52b | |||
|
13afc1d6a4 |
||
92c90753ea | |||
5ab9c501e9 | |||
b445b7c032 | |||
|
832f960531 |
||
ca48ed9e25 | |||
86fd3168f7 | |||
|
7e93c2d766 |
||
|
4ee0b784ea |
||
|
3d717b63e5 |
||
|
cf4e3f609d |
||
|
ea64fba7f6 |
||
|
350e32d0a9 |
||
f9e20fd11e | |||
4fc009813b | |||
|
f66d2d4767 |
||
38eab50b4f | |||
7b5ed58b9b | |||
dd2f0465a4 | |||
c7f9dc4b3d | |||
22baedb24b | |||
4f6f104ddb | |||
|
f9c28ca5fa |
||
73bcc6639b | |||
729d211406 | |||
|
892869ad12 |
||
b00106b7ff | |||
|
8138d388e7 |
||
afcf497989 | |||
893f837b68 | |||
8e2e81651c | |||
|
6553d9510d |
||
84f66a55a5 | |||
a3169d8ffe | |||
e7c5a5e4fa | |||
287f493172 | |||
2f51a0a123 | |||
df8068b79c | |||
7357455cad | |||
f26aa56f42 | |||
357824014e | |||
79a0d58cc5 | |||
3b635c1d6d | |||
|
1d80f531b7 |
||
0ddad8bd9c | |||
f919fb65d6 | |||
8a1e1b7bb9 | |||
6d9d1a4569 | |||
353eda21ac |
764 changed files with 57684 additions and 34205 deletions
45
.cargo/config_fast_builds → .cargo/config_fast_builds.toml
Executable file → Normal file
45
.cargo/config_fast_builds → .cargo/config_fast_builds.toml
Executable file → Normal 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
0
.github/workflows/check.yml
vendored
Executable file → Normal file
71
.github/workflows/doc.yml
vendored
71
.github/workflows/doc.yml
vendored
|
@ -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
12
.gitignore
vendored
Executable file → Normal 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
0
.gitpod.yml
Executable file → Normal file
93
CHANGELOG.md
Normal file
93
CHANGELOG.md
Normal 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
3018
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
119
Cargo.toml
119
Cargo.toml
|
@ -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
37
README.md
Executable file → Normal 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 :).
|
||||
|
|
|
@ -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
4
azalea-auth/README.md
Executable file → Normal 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
0
azalea-auth/examples/auth.rs
Executable file → Normal file
5
azalea-auth/examples/auth_manual.rs
Executable file → Normal file
5
azalea-auth/examples/auth_manual.rs
Executable file → Normal 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
0
azalea-auth/examples/certificates.rs
Executable file → Normal file
150
azalea-auth/src/auth.rs
Executable file → Normal file
150
azalea-auth/src/auth.rs
Executable file → Normal 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
29
azalea-auth/src/cache.rs
Executable file → Normal 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)?;
|
||||
|
|
|
@ -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
26
azalea-auth/src/game_profile.rs
Executable file → Normal 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
0
azalea-auth/src/lib.rs
Executable file → Normal file
7
azalea-auth/src/sessionserver.rs
Executable file → Normal file
7
azalea-auth/src/sessionserver.rs
Executable file → Normal 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
|
||||
|
|
|
@ -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
22
azalea-block/README.md
Executable file → Normal 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>() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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
156
azalea-block/azalea-block-macros/src/lib.rs
Executable file → Normal 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
0
azalea-block/azalea-block-macros/src/utils.rs
Executable file → Normal file
8
azalea-block/src/behavior.rs
Executable file → Normal file
8
azalea-block/src/behavior.rs
Executable file → Normal 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
|
||||
}
|
||||
}
|
||||
|
|
178
azalea-block/src/block_state.rs
Normal file
178
azalea-block/src/block_state.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
137
azalea-block/src/fluid_state.rs
Normal file
137
azalea-block/src/fluid_state.rs
Normal 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
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
203
azalea-block/src/lib.rs
Executable file → Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
0
azalea-brigadier/README.md
Executable file → Normal file
4
azalea-brigadier/src/arguments/argument_type.rs
Executable file → Normal file
4
azalea-brigadier/src/arguments/argument_type.rs
Executable file → Normal 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()
|
||||
|
|
|
@ -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()?))
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
0
azalea-brigadier/src/arguments/mod.rs
Executable file → Normal 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
44
azalea-brigadier/src/builder/argument_builder.rs
Executable file → Normal 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)
|
||||
|
|
2
azalea-brigadier/src/builder/literal_argument_builder.rs
Executable file → Normal file
2
azalea-brigadier/src/builder/literal_argument_builder.rs
Executable file → Normal 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
0
azalea-brigadier/src/builder/mod.rs
Executable file → Normal file
62
azalea-brigadier/src/builder/required_argument_builder.rs
Executable file → Normal file
62
azalea-brigadier/src/builder/required_argument_builder.rs
Executable file → Normal 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
241
azalea-brigadier/src/command_dispatcher.rs
Executable file → Normal 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
79
azalea-brigadier/src/context/command_context.rs
Executable file → Normal 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
42
azalea-brigadier/src/context/command_context_builder.rs
Executable file → Normal 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)
|
||||
|
|
167
azalea-brigadier/src/context/context_chain.rs
Normal file
167
azalea-brigadier/src/context/context_chain.rs
Normal 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
2
azalea-brigadier/src/context/mod.rs
Executable file → Normal 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
3
azalea-brigadier/src/context/parsed_argument.rs
Executable file → Normal 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
3
azalea-brigadier/src/context/parsed_command_node.rs
Executable file → Normal 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
0
azalea-brigadier/src/context/string_range.rs
Executable file → Normal file
71
azalea-brigadier/src/exceptions/builtin_exceptions.rs → azalea-brigadier/src/errors/builtin_errors.rs
Executable file → Normal file
71
azalea-brigadier/src/exceptions/builtin_exceptions.rs → azalea-brigadier/src/errors/builtin_errors.rs
Executable file → Normal 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())
|
||||
}
|
||||
}
|
93
azalea-brigadier/src/errors/command_syntax_error.rs
Normal file
93
azalea-brigadier/src/errors/command_syntax_error.rs
Normal 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())
|
||||
}
|
||||
}
|
5
azalea-brigadier/src/errors/mod.rs
Normal file
5
azalea-brigadier/src/errors/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod builtin_errors;
|
||||
mod command_syntax_error;
|
||||
|
||||
pub use builtin_errors::BuiltInError;
|
||||
pub use command_syntax_error::CommandSyntaxError;
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
3
azalea-brigadier/src/lib.rs
Executable file → Normal 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
4
azalea-brigadier/src/modifier.rs
Executable file → Normal 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
17
azalea-brigadier/src/parse_results.rs
Executable file → Normal 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)
|
||||
|
|
12
azalea-brigadier/src/result_consumer.rs
Normal file
12
azalea-brigadier/src/result_consumer.rs
Normal 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
54
azalea-brigadier/src/string_reader.rs
Executable file → Normal 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
31
azalea-brigadier/src/suggestion/mod.rs
Executable file → Normal 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(())
|
||||
}
|
||||
}
|
||||
|
|
10
azalea-brigadier/src/suggestion/suggestion_provider.rs
Normal file
10
azalea-brigadier/src/suggestion/suggestion_provider.rs
Normal 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
42
azalea-brigadier/src/suggestion/suggestions.rs
Executable file → Normal 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
3
azalea-brigadier/src/suggestion/suggestions_builder.rs
Executable file → Normal 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
67
azalea-brigadier/src/tree/mod.rs
Executable file → Normal 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
|
||||
}
|
||||
|
|
0
azalea-brigadier/tests/arguments/bool_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/bool_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/double_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/double_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/float_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/float_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/integer_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/integer_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/long_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/long_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/string_argument_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/arguments/string_argument_type_test.rs
Executable file → Normal file
189
azalea-brigadier/tests/bevy_app_usage.rs
Normal file
189
azalea-brigadier/tests/bevy_app_usage.rs
Normal 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
12
azalea-brigadier/tests/builder/argument_builder_test.rs
Executable file → Normal 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
|
||||
|
|
0
azalea-brigadier/tests/builder/literal_argument_builder_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/builder/literal_argument_builder_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/builder/required_argument_builder_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/builder/required_argument_builder_test.rs
Executable file → Normal file
54
azalea-brigadier/tests/command_dispatcher_test.rs
Executable file → Normal file
54
azalea-brigadier/tests/command_dispatcher_test.rs
Executable file → Normal 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));
|
||||
}
|
||||
|
||||
|
|
0
azalea-brigadier/tests/command_dispatcher_usages_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/command_dispatcher_usages_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/command_suggestions_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/command_suggestions_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/context/command_context_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/context/command_context_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/context/parsed_argument_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/context/parsed_argument_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs
Executable file → Normal file
52
azalea-brigadier/tests/string_reader_test.rs
Executable file → Normal file
52
azalea-brigadier/tests/string_reader_test.rs
Executable file → Normal 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
0
azalea-brigadier/tests/suggestion/suggestion_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/suggestion/suggestions_builder_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/suggestion/suggestions_builder_test.rs
Executable file → Normal file
2
azalea-brigadier/tests/suggestion/suggestions_test.rs
Executable file → Normal file
2
azalea-brigadier/tests/suggestion/suggestions_test.rs
Executable file → Normal 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);
|
||||
}
|
||||
|
||||
|
|
0
azalea-brigadier/tests/tree/abstract_command_node_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/tree/abstract_command_node_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/tree/argument_command_node_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/tree/argument_command_node_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/tree/literal_command_node_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/tree/literal_command_node_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/tree/root_command_node_test.rs
Executable file → Normal file
0
azalea-brigadier/tests/tree/root_command_node_test.rs
Executable file → Normal 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
0
azalea-buf/README.md
Executable file → Normal 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
0
azalea-buf/azalea-buf-macros/README.md
Executable file → Normal file
22
azalea-buf/azalea-buf-macros/src/lib.rs
Executable file → Normal file
22
azalea-buf/azalea-buf-macros/src/lib.rs
Executable file → Normal 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
|
||||
|
|
|
@ -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()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue