mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 23:44:38 +00:00
Compare commits
No commits in common. "main" and "azalea-block-macros@0.9.1" have entirely different histories.
main
...
azalea-blo
764 changed files with 34138 additions and 57617 deletions
45
.cargo/config_fast_builds.toml → .cargo/config_fast_builds
Normal file → Executable file
45
.cargo/config_fast_builds.toml → .cargo/config_fast_builds
Normal file → Executable file
|
@ -1,46 +1,33 @@
|
|||
# This file is based on Bevy's fast builds config: https://github.com/bevyengine/bevy/blob/main/.cargo/config_fast_builds.toml
|
||||
# This file was borrowed from Bevy: https://github.com/bevyengine/bevy/blob/main/.cargo/config_fast_builds
|
||||
|
||||
# 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 = [
|
||||
# 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",
|
||||
]
|
||||
rustflags = ["-Clink-arg=-fuse-ld=lld", "-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 = ["-Zshare-generics=y"]
|
||||
rustflags = [
|
||||
"-C",
|
||||
"link-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld",
|
||||
"-Zshare-generics=y",
|
||||
]
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = ["-Zshare-generics=y"]
|
||||
rustflags = [
|
||||
"-C",
|
||||
"link-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld",
|
||||
"-Zshare-generics=y",
|
||||
]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "rust-lld.exe"
|
||||
rustflags = [
|
||||
# This needs to be off if you use dynamic linking on Windows.
|
||||
"-Zshare-generics=n",
|
||||
]
|
||||
rustflags = ["-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
Normal file → Executable file
0
.github/workflows/check.yml
vendored
Normal file → Executable file
69
.github/workflows/doc.yml
vendored
69
.github/workflows/doc.yml
vendored
|
@ -1,44 +1,45 @@
|
|||
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
|
||||
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
|
||||
- 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
|
||||
|
|
12
.gitignore
vendored
Normal file → Executable file
12
.gitignore
vendored
Normal file → Executable file
|
@ -1,7 +1,6 @@
|
|||
.idea/
|
||||
.vscode/
|
||||
doc/
|
||||
target/
|
||||
/target
|
||||
/doc
|
||||
.vscode
|
||||
|
||||
.cargo/config
|
||||
.cargo/config.toml
|
||||
|
@ -15,8 +14,3 @@ 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
Normal file → Executable file
0
.gitpod.yml
Normal file → Executable file
93
CHANGELOG.md
93
CHANGELOG.md
|
@ -1,93 +0,0 @@
|
|||
# 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.
|
2974
Cargo.lock
generated
2974
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
119
Cargo.toml
119
Cargo.toml
|
@ -1,111 +1,24 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"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",
|
||||
"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",
|
||||
]
|
||||
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
Normal file → Executable file
37
README.md
Normal file → Executable 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.21.8`._
|
||||
_Currently supported Minecraft version: `1.20.4`._
|
||||
|
||||
> [!WARNING]
|
||||
> Azalea is still unfinished, though most crates are in a useable state.
|
||||
> Azalea is still very unfinished, though most crates are in a somewhat useable state
|
||||
|
||||
## Features
|
||||
|
||||
- [Accurate physics](https://github.com/azalea-rs/azalea/blob/main/azalea-physics/src/lib.rs) (but some features like entity pushing and elytras aren't implemented yet)
|
||||
- [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)
|
||||
- [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)
|
||||
- [Plugins](#plugins)
|
||||
- [Attacking entities](https://azalea.matdoes.dev/azalea/struct.Client.html#method.attack) (but you can't get the entity at the crosshair yet)
|
||||
|
||||
## Docs
|
||||
|
||||
|
@ -49,23 +49,12 @@ If you'd like to chat about Azalea, you can join the Matrix space at [#azalea:ma
|
|||
- Bedrock edition.
|
||||
- Graphics.
|
||||
|
||||
## Real-world bots using Azalea
|
||||
## Branches
|
||||
|
||||
Here's an incomplete list of bots built using Azalea, primarily intended as a reference in addition to the existing documentation and examples:
|
||||
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.
|
||||
|
||||
- [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.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)
|
||||
|
|
|
@ -1,29 +1,33 @@
|
|||
[package]
|
||||
name = "azalea-auth"
|
||||
description = "A port of Mojang's Authlib and launcher authentication."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
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 = [
|
||||
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 = [
|
||||
"json",
|
||||
"rustls-tls",
|
||||
] }
|
||||
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"] }
|
||||
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"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
env_logger = "0.11.2"
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
|
|
4
azalea-auth/README.md
Normal file → Executable file
4
azalea-auth/README.md
Normal file → Executable file
|
@ -2,8 +2,6 @@
|
|||
|
||||
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
|
||||
|
@ -26,4 +24,4 @@ async fn main() {
|
|||
}
|
||||
```
|
||||
|
||||
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).
|
||||
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).
|
||||
|
|
0
azalea-auth/examples/auth.rs
Normal file → Executable file
0
azalea-auth/examples/auth.rs
Normal file → Executable file
5
azalea-auth/examples/auth_manual.rs
Normal file → Executable file
5
azalea-auth/examples/auth_manual.rs
Normal file → Executable file
|
@ -18,16 +18,15 @@ 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, None, None).await?;
|
||||
let res = azalea_auth::get_ms_link_code(&client).await?;
|
||||
println!(
|
||||
"Go to {} and enter the code {}",
|
||||
res.verification_uri, res.user_code
|
||||
);
|
||||
let msa = azalea_auth::get_ms_auth_token(&client, res, None).await?;
|
||||
let msa = azalea_auth::get_ms_auth_token(&client, res).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
Normal file → Executable file
0
azalea-auth/examples/certificates.rs
Normal file → Executable file
152
azalea-auth/src/auth.rs
Normal file → Executable file
152
azalea-auth/src/auth.rs
Normal file → Executable file
|
@ -1,23 +1,19 @@
|
|||
//! Handle Minecraft (Xbox) authentication.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
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},
|
||||
};
|
||||
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<'a> {
|
||||
pub struct AuthOpts {
|
||||
/// 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.
|
||||
|
@ -28,12 +24,6 @@ pub struct AuthOpts<'a> {
|
|||
/// 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)]
|
||||
|
@ -69,16 +59,15 @@ 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 let Some(account) = &cached_account
|
||||
&& !account.mca.is_expired()
|
||||
{
|
||||
if cached_account.is_some() && !cached_account.as_ref().unwrap().mca.is_expired() {
|
||||
let account = cached_account.as_ref().unwrap();
|
||||
// the minecraft auth data is cached and not expired, so we can just
|
||||
// use that instead of doing auth all over again :)
|
||||
|
||||
|
@ -87,38 +76,26 @@ pub async fn auth(email: &str, opts: AuthOpts<'_>) -> Result<AuthResult, AuthErr
|
|||
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, Some(client_id), Some(scope)).await?
|
||||
interactive_get_ms_auth_token(&client, email).await?
|
||||
};
|
||||
if msa.is_expired() {
|
||||
trace!("refreshing Microsoft auth token");
|
||||
match refresh_ms_auth_token(
|
||||
&client,
|
||||
&msa.data.refresh_token,
|
||||
opts.client_id,
|
||||
opts.scope,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::trace!("refreshing Microsoft auth token");
|
||||
match refresh_ms_auth_token(&client, &msa.data.refresh_token).await {
|
||||
Ok(new_msa) => msa = new_msa,
|
||||
Err(e) => {
|
||||
// can't refresh, ask the user to auth again
|
||||
error!("Error refreshing Microsoft auth token: {}", e);
|
||||
msa =
|
||||
interactive_get_ms_auth_token(&client, email, Some(client_id), Some(scope))
|
||||
.await?;
|
||||
tracing::error!("Error refreshing Microsoft auth token: {}", e);
|
||||
msa = interactive_get_ms_auth_token(&client, email).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let msa_token = &msa.data.access_token;
|
||||
trace!("Got access token: {msa_token}");
|
||||
tracing::trace!("Got access token: {msa_token}");
|
||||
|
||||
let res = get_minecraft_token(&client, msa_token).await?;
|
||||
|
||||
|
@ -131,8 +108,8 @@ pub async fn auth(email: &str, opts: AuthOpts<'_>) -> Result<AuthResult, AuthErr
|
|||
|
||||
let profile: ProfileResponse = get_profile(&client, &res.minecraft_access_token).await?;
|
||||
|
||||
if let Some(cache_file) = opts.cache_file
|
||||
&& let Err(e) = cache::set_account_in_cache(
|
||||
if let Some(cache_file) = opts.cache_file {
|
||||
if let Err(e) = cache::set_account_in_cache(
|
||||
&cache_file,
|
||||
email,
|
||||
CachedAccount {
|
||||
|
@ -144,8 +121,9 @@ pub async fn auth(email: &str, opts: AuthOpts<'_>) -> Result<AuthResult, AuthErr
|
|||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("{}", e);
|
||||
{
|
||||
tracing::error!("{}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AuthResult {
|
||||
|
@ -281,7 +259,6 @@ 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 {
|
||||
|
@ -303,12 +280,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, None, None).await?;
|
||||
/// let res = azalea_auth::get_ms_link_code(&client).await?;
|
||||
/// println!(
|
||||
/// "Go to {} and enter the code {}",
|
||||
/// res.verification_uri, res.user_code
|
||||
/// );
|
||||
/// let msa = azalea_auth::get_ms_auth_token(client, res, None).await?;
|
||||
/// let msa = azalea_auth::get_ms_auth_token(client, res).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(())
|
||||
|
@ -316,22 +293,12 @@ 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(&[
|
||||
("scope", scope),
|
||||
("client_id", client_id),
|
||||
.form(&vec![
|
||||
("scope", "service::user.auth.xboxlive.com::MBI_SSL"),
|
||||
("client_id", CLIENT_ID),
|
||||
("response_type", "device_code"),
|
||||
])
|
||||
.send()
|
||||
|
@ -347,37 +314,30 @@ 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 client_id = if let Some(c) = client_id {
|
||||
c
|
||||
} else {
|
||||
CLIENT_ID
|
||||
};
|
||||
|
||||
let login_expires_at = Instant::now() + Duration::from_secs(res.expires_in);
|
||||
let login_expires_at = Instant::now() + std::time::Duration::from_secs(res.expires_in);
|
||||
|
||||
while Instant::now() < login_expires_at {
|
||||
sleep(Duration::from_secs(res.interval)).await;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(res.interval)).await;
|
||||
|
||||
trace!("Polling to check if user has logged in...");
|
||||
let res = client
|
||||
tracing::trace!("Polling to check if user has logged in...");
|
||||
if let Ok(access_token_response) = 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(&[
|
||||
("client_id", client_id),
|
||||
.form(&vec![
|
||||
("client_id", CLIENT_ID),
|
||||
("device_code", &res.device_code),
|
||||
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<AccessTokenResponse>()
|
||||
.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);
|
||||
.await
|
||||
{
|
||||
tracing::trace!("access_token_response: {:?}", access_token_response);
|
||||
let expires_at = SystemTime::now()
|
||||
+ std::time::Duration::from_secs(access_token_response.expires_in);
|
||||
return Ok(ExpiringValue {
|
||||
data: access_token_response,
|
||||
expires_at: expires_at
|
||||
|
@ -397,17 +357,15 @@ 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, client_id, scope).await?;
|
||||
trace!("Device code response: {:?}", res);
|
||||
let res = get_ms_link_code(client).await?;
|
||||
tracing::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, client_id).await
|
||||
get_ms_auth_token(client, res).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
@ -421,17 +379,12 @@ 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(&[
|
||||
("scope", scope),
|
||||
("client_id", client_id),
|
||||
.form(&vec![
|
||||
("scope", "service::user.auth.xboxlive.com::MBI_SSL"),
|
||||
("client_id", CLIENT_ID),
|
||||
("grant_type", "refresh_token"),
|
||||
("refresh_token", refresh_token),
|
||||
])
|
||||
|
@ -442,7 +395,8 @@ 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() + Duration::from_secs(access_token_response.expires_in);
|
||||
let expires_at =
|
||||
SystemTime::now() + std::time::Duration::from_secs(access_token_response.expires_in);
|
||||
Ok(ExpiringValue {
|
||||
data: access_token_response,
|
||||
expires_at: expires_at
|
||||
|
@ -476,7 +430,7 @@ async fn auth_with_xbox_live(
|
|||
"TokenType": "JWT"
|
||||
});
|
||||
let payload = auth_json.to_string();
|
||||
trace!("auth_json: {:#?}", auth_json);
|
||||
tracing::trace!("auth_json: {:#?}", auth_json);
|
||||
let res = client
|
||||
.post("https://user.auth.xboxlive.com/user/authenticate")
|
||||
.header("Content-Type", "application/json")
|
||||
|
@ -489,7 +443,7 @@ async fn auth_with_xbox_live(
|
|||
.await?
|
||||
.json::<XboxLiveAuthResponse>()
|
||||
.await?;
|
||||
trace!("Xbox Live auth response: {:?}", res);
|
||||
tracing::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)
|
||||
|
@ -530,7 +484,7 @@ async fn obtain_xsts_for_minecraft(
|
|||
.await?
|
||||
.json::<XboxLiveAuthResponse>()
|
||||
.await?;
|
||||
trace!("Xbox Live auth response (for XSTS): {:?}", res);
|
||||
tracing::trace!("Xbox Live auth response (for XSTS): {:?}", res);
|
||||
|
||||
Ok(res.token)
|
||||
}
|
||||
|
@ -556,9 +510,9 @@ async fn auth_with_minecraft(
|
|||
.await?
|
||||
.json::<MinecraftAuthResponse>()
|
||||
.await?;
|
||||
trace!("{:?}", res);
|
||||
tracing::trace!("{:?}", res);
|
||||
|
||||
let expires_at = SystemTime::now() + Duration::from_secs(res.expires_in);
|
||||
let expires_at = SystemTime::now() + std::time::Duration::from_secs(res.expires_in);
|
||||
Ok(ExpiringValue {
|
||||
data: res,
|
||||
// to seconds since epoch
|
||||
|
@ -583,7 +537,7 @@ pub async fn check_ownership(
|
|||
.await?
|
||||
.json::<GameOwnershipResponse>()
|
||||
.await?;
|
||||
trace!("{:?}", res);
|
||||
tracing::trace!("{:?}", res);
|
||||
|
||||
// vanilla checks here to make sure the signatures are right, but it's not
|
||||
// actually required so we just don't
|
||||
|
@ -608,7 +562,7 @@ pub async fn get_profile(
|
|||
.await?
|
||||
.json::<ProfileResponse>()
|
||||
.await?;
|
||||
trace!("{:?}", res);
|
||||
tracing::trace!("{:?}", res);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
|
29
azalea-auth/src/cache.rs
Normal file → Executable file
29
azalea-auth/src/cache.rs
Normal file → Executable file
|
@ -1,27 +1,20 @@
|
|||
//! 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::{self, File},
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
};
|
||||
use tracing::{debug, trace};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CacheError {
|
||||
#[error("Failed to read cache file: {0}")]
|
||||
Read(io::Error),
|
||||
Read(std::io::Error),
|
||||
#[error("Failed to write cache file: {0}")]
|
||||
Write(io::Error),
|
||||
Write(std::io::Error),
|
||||
#[error("Failed to create cache file directory: {0}")]
|
||||
MkDir(io::Error),
|
||||
MkDir(std::io::Error),
|
||||
#[error("Failed to parse cache file: {0}")]
|
||||
Parse(serde_json::Error),
|
||||
}
|
||||
|
@ -89,19 +82,17 @@ 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> {
|
||||
trace!("saving cache: {:?}", cache);
|
||||
tracing::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.");
|
||||
debug!(
|
||||
tracing::debug!(
|
||||
"Making cache file parent directory at {}",
|
||||
cache_file_parent.to_string_lossy()
|
||||
);
|
||||
fs::create_dir_all(cache_file_parent)
|
||||
.await
|
||||
.map_err(CacheError::MkDir)?;
|
||||
std::fs::create_dir_all(cache_file_parent).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,9 +1,8 @@
|
|||
use base64::Engine;
|
||||
use chrono::{DateTime, Utc};
|
||||
use rsa::{RsaPrivateKey, pkcs8::DecodePrivateKey};
|
||||
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use tracing::trace;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FetchCertificatesError {
|
||||
|
@ -27,7 +26,7 @@ pub async fn fetch_certificates(
|
|||
.await?
|
||||
.json::<CertificatesResponse>()
|
||||
.await?;
|
||||
trace!("{:?}", res);
|
||||
tracing::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
Normal file → Executable file
26
azalea-auth/src/game_profile.rs
Normal file → Executable file
|
@ -1,17 +1,15 @@
|
|||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use azalea_buf::AzBuf;
|
||||
use azalea_buf::McBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(AzBuf, Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[derive(McBuf, Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub struct GameProfile {
|
||||
/// The UUID of the player.
|
||||
pub uuid: Uuid,
|
||||
/// The username of the player.
|
||||
pub name: String,
|
||||
// this is an arc to make GameProfile cheaper to clone when the properties are big
|
||||
pub properties: Arc<HashMap<String, ProfilePropertyValue>>,
|
||||
pub properties: HashMap<String, ProfilePropertyValue>,
|
||||
}
|
||||
|
||||
impl GameProfile {
|
||||
|
@ -19,7 +17,7 @@ impl GameProfile {
|
|||
GameProfile {
|
||||
uuid,
|
||||
name,
|
||||
properties: Arc::new(HashMap::new()),
|
||||
properties: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,12 +37,12 @@ impl From<SerializableGameProfile> for GameProfile {
|
|||
Self {
|
||||
uuid: value.id,
|
||||
name: value.name,
|
||||
properties: Arc::new(properties),
|
||||
properties,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(AzBuf, Debug, Clone, Eq, PartialEq)]
|
||||
#[derive(McBuf, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct ProfilePropertyValue {
|
||||
pub value: String,
|
||||
pub signature: Option<String>,
|
||||
|
@ -60,11 +58,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.clone(),
|
||||
value: value.value.clone(),
|
||||
signature: value.signature.clone(),
|
||||
name: key,
|
||||
value: value.value,
|
||||
signature: value.signature,
|
||||
});
|
||||
}
|
||||
Self {
|
||||
|
@ -115,7 +113,7 @@ mod tests {
|
|||
signature: Some("zxcv".to_string()),
|
||||
},
|
||||
);
|
||||
map.into()
|
||||
map
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
0
azalea-auth/src/lib.rs
Normal file → Executable file
0
azalea-auth/src/lib.rs
Normal file → Executable file
7
azalea-auth/src/sessionserver.rs
Normal file → Executable file
7
azalea-auth/src/sessionserver.rs
Normal file → Executable file
|
@ -1,6 +1,5 @@
|
|||
//! Tell Mojang you're joining a multiplayer server.
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
@ -50,7 +49,7 @@ pub struct ForbiddenError {
|
|||
pub path: String,
|
||||
}
|
||||
|
||||
static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(reqwest::Client::new);
|
||||
static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::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
|
||||
|
@ -159,7 +158,7 @@ pub async fn serverside_auth(
|
|||
StatusCode::FORBIDDEN => {
|
||||
return Err(ServerSessionServerError::Unknown(
|
||||
res.json::<ForbiddenError>().await?.error,
|
||||
));
|
||||
))
|
||||
}
|
||||
status_code => {
|
||||
// log the headers
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
[package]
|
||||
name = "azalea-block"
|
||||
description = "Representation of Minecraft block states."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
azalea-block-macros.workspace = true
|
||||
azalea-buf.workspace = true
|
||||
azalea-registry.workspace = true
|
||||
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" }
|
||||
|
|
22
azalea-block/README.md
Normal file → Executable file
22
azalea-block/README.md
Normal file → Executable 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::WallEast::Low,
|
||||
north: azalea_block::properties::WallNorth::Low,
|
||||
south: azalea_block::properties::WallSouth::Low,
|
||||
west: azalea_block::properties::WallWest::Low,
|
||||
east: azalea_block::properties::EastWall::Low,
|
||||
north: azalea_block::properties::NorthWall::Low,
|
||||
south: azalea_block::properties::SouthWall::Low,
|
||||
west: azalea_block::properties::WestWall::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();
|
||||
```
|
||||
|
||||
## BlockTrait
|
||||
## Block trait
|
||||
|
||||
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`.
|
||||
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.
|
||||
|
||||
```
|
||||
# use azalea_block::{BlockTrait, BlockState};
|
||||
# use azalea_block::{Block, BlockState};
|
||||
# let block_state = BlockState::from(azalea_registry::Block::Jukebox);
|
||||
let block = Box::<dyn BlockTrait>::from(block_state);
|
||||
let block = Box::<dyn Block>::from(block_state);
|
||||
```
|
||||
```
|
||||
# use azalea_block::{BlockTrait, BlockState};
|
||||
# use azalea_block::{Block, BlockState};
|
||||
# let block_state: BlockState = azalea_registry::Block::Jukebox.into();
|
||||
if let Some(jukebox) = Box::<dyn BlockTrait>::from(block_state).downcast_ref::<azalea_block::blocks::Jukebox>() {
|
||||
if let Some(jukebox) = Box::<dyn Block>::from(block_state).downcast_ref::<azalea_block::blocks::Jukebox>() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
[package]
|
||||
name = "azalea-block-macros"
|
||||
description = "Proc macros used by azalea-block."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
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"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proc-macro2.workspace = true
|
||||
quote.workspace = true
|
||||
syn.workspace = true
|
||||
proc-macro2 = "1.0.78"
|
||||
quote = "1.0.35"
|
||||
syn = "2.0.49"
|
||||
|
|
156
azalea-block/azalea-block-macros/src/lib.rs
Normal file → Executable file
156
azalea-block/azalea-block-macros/src/lib.rs
Normal file → Executable file
|
@ -2,24 +2,22 @@
|
|||
|
||||
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::{
|
||||
Expr, Ident, LitStr, Token, braced,
|
||||
braced,
|
||||
ext::IdentExt,
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream, Result},
|
||||
parse_macro_input,
|
||||
punctuated::Punctuated,
|
||||
token,
|
||||
token, Expr, Ident, LitStr, 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 {
|
||||
|
@ -41,14 +39,11 @@ 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: String,
|
||||
/// The property name, potentially modified so it works better as a struct
|
||||
/// field.
|
||||
name_ident: Ident,
|
||||
// "snowy" / "axis"
|
||||
name: Ident,
|
||||
// Snowy / Axis
|
||||
property_type: Ident,
|
||||
property_value_type: Ident,
|
||||
|
@ -59,7 +54,7 @@ struct PropertyWithNameAndDefault {
|
|||
|
||||
/// ```ignore
|
||||
/// grass_block => BlockBehavior::default(), {
|
||||
/// "snowy": false,
|
||||
/// snowy: false,
|
||||
/// },
|
||||
/// ```
|
||||
struct BlockDefinition {
|
||||
|
@ -69,8 +64,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::<LitStr>()?.value();
|
||||
// `snowy: Snowy(false)` or `axis: properties::Axis::Y`
|
||||
let property_name = input.parse()?;
|
||||
input.parse::<Token![:]>()?;
|
||||
|
||||
let first_ident = input.call(Ident::parse_any)?;
|
||||
|
@ -105,11 +100,8 @@ 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,
|
||||
|
@ -204,11 +196,11 @@ impl Parse for PropertyDefinitions {
|
|||
|
||||
impl Parse for BlockDefinition {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
// acacia_button => BlockBehavior::new().strength(0.5, 0.5), {
|
||||
// "face": Face::Wall,
|
||||
// "facing": FacingCardinal::North,
|
||||
// "powered": Powered(false),
|
||||
// },
|
||||
// acacia_button => BlockBehavior::default(), {
|
||||
// Facing=North,
|
||||
// Powered=False,
|
||||
// Face=Wall,
|
||||
// }
|
||||
let name = input.parse()?;
|
||||
input.parse::<Token![=>]>()?;
|
||||
let behavior = input.parse()?;
|
||||
|
@ -276,7 +268,7 @@ impl Parse for MakeBlockStates {
|
|||
}
|
||||
|
||||
struct PropertyVariantData {
|
||||
pub block_state_ids: Vec<BlockStateIntegerRepr>,
|
||||
pub block_state_ids: Vec<u32>,
|
||||
pub ident: Ident,
|
||||
pub is_enum: bool,
|
||||
}
|
||||
|
@ -289,7 +281,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: BlockStateIntegerRepr = 0;
|
||||
let mut state_id: u32 = 0;
|
||||
|
||||
for property in &input.property_definitions.properties {
|
||||
let property_struct_name: Ident;
|
||||
|
@ -340,8 +332,8 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
#property_enum_variants
|
||||
}
|
||||
|
||||
impl From<crate::block_state::BlockStateIntegerRepr> for #property_struct_name {
|
||||
fn from(value: crate::block_state::BlockStateIntegerRepr) -> Self {
|
||||
impl From<u32> for #property_struct_name {
|
||||
fn from(value: u32) -> Self {
|
||||
match value {
|
||||
#property_from_number_variants
|
||||
_ => panic!("Invalid property value: {}", value),
|
||||
|
@ -359,8 +351,8 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct #property_struct_name(pub bool);
|
||||
|
||||
impl From<crate::block_state::BlockStateIntegerRepr> for #property_struct_name {
|
||||
fn from(value: crate::block_state::BlockStateIntegerRepr) -> Self {
|
||||
impl From<u32> for #property_struct_name {
|
||||
fn from(value: u32) -> Self {
|
||||
match value {
|
||||
0 => Self(false),
|
||||
1 => Self(true),
|
||||
|
@ -424,25 +416,29 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
Some(
|
||||
previous_names
|
||||
.iter()
|
||||
.filter(|&p| p == &property.name)
|
||||
.filter(|&p| p == &property.name.to_string())
|
||||
.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)
|
||||
.get(&property.name.to_string())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| property.name.clone());
|
||||
.unwrap_or_else(|| property.name.to_string());
|
||||
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: name_to_ident(&property_name),
|
||||
name: property_name,
|
||||
name: Ident::new(&property_name, proc_macro2::Span::call_site()),
|
||||
property_type: property.property_type.clone(),
|
||||
property_value_type: property.property_value_type.clone(),
|
||||
is_enum: property.is_enum,
|
||||
|
@ -461,15 +457,17 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
let mut block_struct_fields = quote! {};
|
||||
for PropertyWithNameAndDefault {
|
||||
property_value_type,
|
||||
name_ident,
|
||||
name,
|
||||
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_ident: properties::#property_value_type, }
|
||||
quote! { pub #name: properties::#property_value_type, }
|
||||
} else {
|
||||
quote! { pub #name_ident: #property_value_type, }
|
||||
quote! { pub #name: #property_value_type, }
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -504,19 +502,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_ident = &property.name_ident;
|
||||
let property_name = &property.name;
|
||||
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 =
|
||||
match property.default.clone().into_iter().last().unwrap() {
|
||||
TokenTree::Ident(ident) => ident.to_string(),
|
||||
_ => {
|
||||
panic!()
|
||||
}
|
||||
};
|
||||
let property_default_as_string = if let TokenTree::Ident(ident) =
|
||||
property.default.clone().into_iter().last().unwrap()
|
||||
{
|
||||
ident.to_string()
|
||||
} else {
|
||||
panic!()
|
||||
};
|
||||
if property_default_as_string != combination[i] {
|
||||
is_default = false;
|
||||
}
|
||||
|
@ -528,7 +526,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
};
|
||||
|
||||
from_block_to_state_combination_match_inner.extend(quote! {
|
||||
#property_name_ident: #property_variant,
|
||||
#property_name: #property_variant,
|
||||
});
|
||||
|
||||
// add to properties_to_state_ids
|
||||
|
@ -551,7 +549,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::new_const(#state_id),
|
||||
} => BlockState { id: #state_id },
|
||||
});
|
||||
|
||||
if is_default {
|
||||
|
@ -564,16 +562,15 @@ 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| match p.default.clone().into_iter().last().unwrap() {
|
||||
TokenTree::Ident(i) => i.to_string(),
|
||||
_ => {
|
||||
.map(|p| {
|
||||
if let TokenTree::Ident(i) = p.default.clone().into_iter().last().unwrap() {
|
||||
i.to_string()
|
||||
} else {
|
||||
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 => {
|
||||
|
@ -585,17 +582,17 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
// }
|
||||
// }
|
||||
let mut from_state_to_block_inner = quote! {};
|
||||
let mut division: BlockStateIntegerRepr = 1;
|
||||
let mut division = 1u32;
|
||||
for i in (0..properties_with_name.len()).rev() {
|
||||
let PropertyWithNameAndDefault {
|
||||
property_type: property_struct_name_ident,
|
||||
name_ident: property_name_ident,
|
||||
name: property_name,
|
||||
property_value_type,
|
||||
..
|
||||
} = &properties_with_name[i];
|
||||
|
||||
let property_variants = &block_properties_vec[i];
|
||||
let property_variants_count = property_variants.len() as crate::BlockStateIntegerRepr;
|
||||
let property_variants_count = property_variants.len() as u32;
|
||||
let conversion_code = {
|
||||
if &property_value_type.to_string() == "bool" {
|
||||
assert_eq!(property_variants_count, 2);
|
||||
|
@ -606,7 +603,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
}
|
||||
};
|
||||
from_state_to_block_inner.extend(quote! {
|
||||
#property_name_ident: #conversion_code,
|
||||
#property_name: #conversion_code,
|
||||
});
|
||||
|
||||
division *= property_variants_count;
|
||||
|
@ -625,7 +622,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::new_const(#default_state_id),
|
||||
azalea_registry::Block::#block_name_pascal_case => BlockState { id: #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),
|
||||
|
@ -633,19 +630,19 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
|
||||
let mut block_default_fields = quote! {};
|
||||
for PropertyWithNameAndDefault {
|
||||
name_ident,
|
||||
name,
|
||||
default: property_default,
|
||||
..
|
||||
} in properties_with_name
|
||||
{
|
||||
block_default_fields.extend(quote! { #name_ident: #property_default, });
|
||||
block_default_fields.extend(quote! { #name: #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::new_const(#first_state_id) }
|
||||
quote! { BlockState { id: #first_state_id } }
|
||||
} else {
|
||||
quote! {
|
||||
match self {
|
||||
|
@ -660,7 +657,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
#block_struct_fields
|
||||
}
|
||||
|
||||
impl BlockTrait for #block_struct_name {
|
||||
impl Block for #block_struct_name {
|
||||
fn behavior(&self) -> BlockBehavior {
|
||||
#block_behavior
|
||||
}
|
||||
|
@ -696,8 +693,11 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
let last_state_id = state_id - 1;
|
||||
let mut generated = quote! {
|
||||
impl BlockState {
|
||||
/// The highest possible block state ID.
|
||||
pub const MAX_STATE: crate::block_state::BlockStateIntegerRepr = #last_state_id;
|
||||
/// Returns the highest possible state ID.
|
||||
#[inline]
|
||||
pub fn max_state() -> u32 {
|
||||
#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 BlockTrait> {
|
||||
impl From<BlockState> for Box<dyn Block> {
|
||||
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 BlockTrait> {
|
||||
impl From<azalea_registry::Block> for Box<dyn Block> {
|
||||
fn from(block: azalea_registry::Block) -> Self {
|
||||
match block {
|
||||
#from_registry_block_to_block_match
|
||||
|
@ -823,13 +823,3 @@ 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
Normal file → Executable file
0
azalea-block/azalea-block-macros/src/utils.rs
Normal file → Executable file
8
azalea-block/src/behavior.rs
Normal file → Executable file
8
azalea-block/src/behavior.rs
Normal file → Executable file
|
@ -4,8 +4,6 @@ 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 {
|
||||
|
@ -16,7 +14,6 @@ impl Default for BlockBehavior {
|
|||
destroy_time: 0.,
|
||||
explosion_resistance: 0.,
|
||||
requires_correct_tool_for_drops: false,
|
||||
force_solid: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,9 +52,4 @@ 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,178 +0,0 @@
|
|||
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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
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
Normal file → Executable file
4214
azalea-block/src/generated.rs
Normal file → Executable file
File diff suppressed because it is too large
Load diff
203
azalea-block/src/lib.rs
Normal file → Executable file
203
azalea-block/src/lib.rs
Normal file → Executable file
|
@ -1,21 +1,22 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![feature(trait_upcasting)]
|
||||
|
||||
mod behavior;
|
||||
pub mod block_state;
|
||||
pub mod fluid_state;
|
||||
mod generated;
|
||||
mod range;
|
||||
|
||||
use core::fmt::Debug;
|
||||
use std::any::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 {
|
||||
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},
|
||||
};
|
||||
|
||||
pub trait Block: Debug + Any {
|
||||
fn behavior(&self) -> BlockBehavior;
|
||||
/// Get the Minecraft ID for this block. For example `stone` or
|
||||
/// `grass_block`.
|
||||
|
@ -27,8 +28,8 @@ pub trait BlockTrait: Debug + Any {
|
|||
/// `azalea_registry::Block` doesn't contain any state data.
|
||||
fn as_registry_block(&self) -> azalea_registry::Block;
|
||||
}
|
||||
impl dyn BlockTrait {
|
||||
pub fn downcast_ref<T: BlockTrait>(&self) -> Option<&T> {
|
||||
impl dyn Block {
|
||||
pub fn downcast_ref<T: Block>(&self) -> Option<&T> {
|
||||
(self as &dyn Any).downcast_ref::<T>()
|
||||
}
|
||||
}
|
||||
|
@ -38,3 +39,181 @@ 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,22 +1,20 @@
|
|||
use std::{
|
||||
collections::{HashSet, hash_set},
|
||||
collections::HashSet,
|
||||
ops::{Add, RangeInclusive},
|
||||
};
|
||||
|
||||
use azalea_registry::Block;
|
||||
|
||||
use crate::{BlockState, block_state::BlockStateIntegerRepr};
|
||||
use crate::BlockState;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockStates {
|
||||
pub set: HashSet<BlockState>,
|
||||
}
|
||||
|
||||
impl From<RangeInclusive<BlockStateIntegerRepr>> for BlockStates {
|
||||
fn from(range: RangeInclusive<BlockStateIntegerRepr>) -> Self {
|
||||
impl From<RangeInclusive<u32>> for BlockStates {
|
||||
fn from(range: RangeInclusive<u32>) -> Self {
|
||||
let mut set = HashSet::with_capacity((range.end() - range.start() + 1) as usize);
|
||||
for id in range {
|
||||
set.insert(BlockState::try_from(id).unwrap_or_default());
|
||||
set.insert(BlockState { id });
|
||||
}
|
||||
Self { set }
|
||||
}
|
||||
|
@ -24,7 +22,7 @@ impl From<RangeInclusive<BlockStateIntegerRepr>> for BlockStates {
|
|||
|
||||
impl IntoIterator for BlockStates {
|
||||
type Item = BlockState;
|
||||
type IntoIter = hash_set::IntoIter<BlockState>;
|
||||
type IntoIter = std::collections::hash_set::IntoIter<BlockState>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.set.into_iter()
|
||||
|
@ -46,34 +44,3 @@ 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,19 +1,17 @@
|
|||
[package]
|
||||
name = "azalea-brigadier"
|
||||
description = "A port of Mojang's Brigadier command parsing and dispatching library."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-brigadier"
|
||||
repository = "https://github.com/azalea-rs/azalea/tree/main/azalea-brigadier"
|
||||
version = "0.9.1"
|
||||
|
||||
[dev-dependencies]
|
||||
bevy_app.workspace = true
|
||||
bevy_ecs.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
azalea-buf = { workspace = true, optional = true }
|
||||
azalea-chat = { workspace = true, optional = true }
|
||||
parking_lot.workspace = true
|
||||
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"
|
||||
|
||||
[features]
|
||||
azalea-buf = ["dep:azalea-buf", "dep:azalea-chat", "azalea-chat/azalea-buf"]
|
||||
azalea-buf = ["dep:azalea-buf", "dep:azalea-chat"]
|
||||
|
|
0
azalea-brigadier/README.md
Normal file → Executable file
0
azalea-brigadier/README.md
Normal file → Executable file
4
azalea-brigadier/src/arguments/argument_type.rs
Normal file → Executable file
4
azalea-brigadier/src/arguments/argument_type.rs
Normal file → Executable file
|
@ -1,13 +1,13 @@
|
|||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
errors::CommandSyntaxError,
|
||||
exceptions::CommandSyntaxException,
|
||||
string_reader::StringReader,
|
||||
suggestion::{Suggestions, SuggestionsBuilder},
|
||||
};
|
||||
|
||||
pub trait ArgumentType {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError>;
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException>;
|
||||
|
||||
fn list_suggestions(&self, _builder: SuggestionsBuilder) -> Suggestions {
|
||||
Suggestions::default()
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use super::ArgumentType;
|
||||
use crate::{
|
||||
context::CommandContext,
|
||||
errors::CommandSyntaxError,
|
||||
exceptions::CommandSyntaxException,
|
||||
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>, CommandSyntaxError> {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
|
||||
Ok(Arc::new(reader.read_boolean()?))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use super::ArgumentType;
|
||||
use crate::{
|
||||
context::CommandContext,
|
||||
errors::{BuiltInError, CommandSyntaxError},
|
||||
exceptions::{BuiltInExceptions, CommandSyntaxException},
|
||||
string_reader::StringReader,
|
||||
};
|
||||
|
||||
use super::ArgumentType;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Double {
|
||||
pub minimum: Option<f64>,
|
||||
|
@ -14,28 +15,28 @@ struct Double {
|
|||
}
|
||||
|
||||
impl ArgumentType for Double {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
|
||||
let start = reader.cursor;
|
||||
let result = reader.read_double()?;
|
||||
if let Some(minimum) = self.minimum
|
||||
&& result < minimum
|
||||
{
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInError::DoubleTooSmall {
|
||||
found: result,
|
||||
min: minimum,
|
||||
if let Some(minimum) = self.minimum {
|
||||
if result < minimum {
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInExceptions::DoubleTooSmall {
|
||||
found: result,
|
||||
min: minimum,
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
if let Some(maximum) = self.maximum
|
||||
&& result > maximum
|
||||
{
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInError::DoubleTooBig {
|
||||
found: result,
|
||||
max: maximum,
|
||||
if let Some(maximum) = self.maximum {
|
||||
if result > maximum {
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInExceptions::DoubleTooBig {
|
||||
found: result,
|
||||
max: maximum,
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
Ok(Arc::new(result))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use super::ArgumentType;
|
||||
use crate::{
|
||||
context::CommandContext,
|
||||
errors::{BuiltInError, CommandSyntaxError},
|
||||
exceptions::{BuiltInExceptions, CommandSyntaxException},
|
||||
string_reader::StringReader,
|
||||
};
|
||||
|
||||
use super::ArgumentType;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Float {
|
||||
pub minimum: Option<f32>,
|
||||
|
@ -14,28 +15,28 @@ struct Float {
|
|||
}
|
||||
|
||||
impl ArgumentType for Float {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
|
||||
let start = reader.cursor;
|
||||
let result = reader.read_float()?;
|
||||
if let Some(minimum) = self.minimum
|
||||
&& result < minimum
|
||||
{
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInError::FloatTooSmall {
|
||||
found: result,
|
||||
min: minimum,
|
||||
if let Some(minimum) = self.minimum {
|
||||
if result < minimum {
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInExceptions::FloatTooSmall {
|
||||
found: result,
|
||||
min: minimum,
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
if let Some(maximum) = self.maximum
|
||||
&& result > maximum
|
||||
{
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInError::FloatTooBig {
|
||||
found: result,
|
||||
max: maximum,
|
||||
if let Some(maximum) = self.maximum {
|
||||
if result > maximum {
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInExceptions::FloatTooBig {
|
||||
found: result,
|
||||
max: maximum,
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
Ok(Arc::new(result))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use super::ArgumentType;
|
||||
use crate::{
|
||||
context::CommandContext,
|
||||
errors::{BuiltInError, CommandSyntaxError},
|
||||
exceptions::{BuiltInExceptions, CommandSyntaxException},
|
||||
string_reader::StringReader,
|
||||
};
|
||||
|
||||
use super::ArgumentType;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Integer {
|
||||
pub minimum: Option<i32>,
|
||||
|
@ -14,28 +15,28 @@ struct Integer {
|
|||
}
|
||||
|
||||
impl ArgumentType for Integer {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
|
||||
let start = reader.cursor;
|
||||
let result = reader.read_int()?;
|
||||
if let Some(minimum) = self.minimum
|
||||
&& result < minimum
|
||||
{
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInError::IntegerTooSmall {
|
||||
found: result,
|
||||
min: minimum,
|
||||
if let Some(minimum) = self.minimum {
|
||||
if result < minimum {
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInExceptions::IntegerTooSmall {
|
||||
found: result,
|
||||
min: minimum,
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
if let Some(maximum) = self.maximum
|
||||
&& result > maximum
|
||||
{
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInError::IntegerTooBig {
|
||||
found: result,
|
||||
max: maximum,
|
||||
if let Some(maximum) = self.maximum {
|
||||
if result > maximum {
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInExceptions::IntegerTooBig {
|
||||
found: result,
|
||||
max: maximum,
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
Ok(Arc::new(result))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use super::ArgumentType;
|
||||
use crate::{
|
||||
context::CommandContext,
|
||||
errors::{BuiltInError, CommandSyntaxError},
|
||||
exceptions::{BuiltInExceptions, CommandSyntaxException},
|
||||
string_reader::StringReader,
|
||||
};
|
||||
|
||||
use super::ArgumentType;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Long {
|
||||
pub minimum: Option<i64>,
|
||||
|
@ -14,28 +15,28 @@ struct Long {
|
|||
}
|
||||
|
||||
impl ArgumentType for Long {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
|
||||
let start = reader.cursor;
|
||||
let result = reader.read_long()?;
|
||||
if let Some(minimum) = self.minimum
|
||||
&& result < minimum
|
||||
{
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInError::LongTooSmall {
|
||||
found: result,
|
||||
min: minimum,
|
||||
if let Some(minimum) = self.minimum {
|
||||
if result < minimum {
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInExceptions::LongTooSmall {
|
||||
found: result,
|
||||
min: minimum,
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
if let Some(maximum) = self.maximum
|
||||
&& result > maximum
|
||||
{
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInError::LongTooBig {
|
||||
found: result,
|
||||
max: maximum,
|
||||
if let Some(maximum) = self.maximum {
|
||||
if result > maximum {
|
||||
reader.cursor = start;
|
||||
return Err(BuiltInExceptions::LongTooBig {
|
||||
found: result,
|
||||
max: maximum,
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
.create_with_context(reader));
|
||||
}
|
||||
Ok(Arc::new(result))
|
||||
}
|
||||
|
|
0
azalea-brigadier/src/arguments/mod.rs
Normal file → Executable file
0
azalea-brigadier/src/arguments/mod.rs
Normal file → Executable file
|
@ -1,7 +1,10 @@
|
|||
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.
|
||||
|
@ -14,7 +17,7 @@ pub enum StringArgument {
|
|||
}
|
||||
|
||||
impl ArgumentType for StringArgument {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
|
||||
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
|
||||
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
Normal file → Executable file
44
azalea-brigadier/src/builder/argument_builder.rs
Normal file → Executable file
|
@ -1,32 +1,18 @@
|
|||
use std::{
|
||||
fmt::{self, Debug},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use super::{literal_argument_builder::Literal, required_argument_builder::Argument};
|
||||
use crate::{
|
||||
context::CommandContext,
|
||||
errors::CommandSyntaxError,
|
||||
modifier::RedirectModifier,
|
||||
tree::{Command, CommandNode},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ArgumentBuilderType<S> {
|
||||
use super::{literal_argument_builder::Literal, required_argument_builder::Argument};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ArgumentBuilderType {
|
||||
Literal(Literal),
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
Argument(Argument),
|
||||
}
|
||||
|
||||
/// A node that hasn't yet been built.
|
||||
|
@ -44,7 +30,7 @@ pub struct ArgumentBuilder<S> {
|
|||
|
||||
/// A node that isn't yet built.
|
||||
impl<S> ArgumentBuilder<S> {
|
||||
pub fn new(value: ArgumentBuilderType<S>) -> Self {
|
||||
pub fn new(value: ArgumentBuilderType) -> Self {
|
||||
Self {
|
||||
arguments: CommandNode {
|
||||
value,
|
||||
|
@ -63,7 +49,9 @@ 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 {
|
||||
|
@ -91,16 +79,6 @@ 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
|
||||
|
@ -185,7 +163,7 @@ impl<S> ArgumentBuilder<S> {
|
|||
}
|
||||
|
||||
impl<S> Debug for ArgumentBuilder<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ArgumentBuilder")
|
||||
.field("arguments", &self.arguments)
|
||||
// .field("command", &self.command)
|
||||
|
|
2
azalea-brigadier/src/builder/literal_argument_builder.rs
Normal file → Executable file
2
azalea-brigadier/src/builder/literal_argument_builder.rs
Normal file → Executable file
|
@ -12,7 +12,7 @@ impl Literal {
|
|||
}
|
||||
}
|
||||
|
||||
impl<S> From<Literal> for ArgumentBuilderType<S> {
|
||||
impl From<Literal> for ArgumentBuilderType {
|
||||
fn from(literal: Literal) -> Self {
|
||||
Self::Literal(literal)
|
||||
}
|
||||
|
|
0
azalea-brigadier/src/builder/mod.rs
Normal file → Executable file
0
azalea-brigadier/src/builder/mod.rs
Normal file → Executable file
62
azalea-brigadier/src/builder/required_argument_builder.rs
Normal file → Executable file
62
azalea-brigadier/src/builder/required_argument_builder.rs
Normal file → Executable file
|
@ -1,52 +1,36 @@
|
|||
use std::{
|
||||
any::Any,
|
||||
fmt::{self, Debug},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use super::argument_builder::{ArgumentBuilder, ArgumentBuilderType};
|
||||
use crate::{
|
||||
arguments::ArgumentType,
|
||||
context::CommandContext,
|
||||
errors::CommandSyntaxError,
|
||||
exceptions::CommandSyntaxException,
|
||||
string_reader::StringReader,
|
||||
suggestion::{SuggestionProvider, Suggestions, SuggestionsBuilder},
|
||||
suggestion::{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.
|
||||
pub struct Argument<S> {
|
||||
#[derive(Clone)]
|
||||
pub struct Argument {
|
||||
pub name: String,
|
||||
parser: Arc<dyn ArgumentType + Send + Sync>,
|
||||
custom_suggestions: Option<Arc<dyn SuggestionProvider<S> + Send + Sync>>,
|
||||
}
|
||||
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 {
|
||||
impl Argument {
|
||||
pub fn new(name: &str, parser: Arc<dyn ArgumentType + Send + Sync>) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
parser,
|
||||
custom_suggestions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxError> {
|
||||
pub fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
|
||||
self.parser.parse(reader)
|
||||
}
|
||||
|
||||
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 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 examples(&self) -> Vec<String> {
|
||||
|
@ -54,14 +38,14 @@ impl<S> Argument<S> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<S> From<Argument<S>> for ArgumentBuilderType<S> {
|
||||
fn from(argument: Argument<S>) -> Self {
|
||||
impl From<Argument> for ArgumentBuilderType {
|
||||
fn from(argument: Argument) -> Self {
|
||||
Self::Argument(argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Debug for Argument<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
impl Debug for Argument {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Argument")
|
||||
.field("name", &self.name)
|
||||
// .field("parser", &self.parser)
|
||||
|
@ -74,15 +58,5 @@ pub fn argument<S>(
|
|||
name: &str,
|
||||
parser: impl ArgumentType + Send + Sync + 'static,
|
||||
) -> ArgumentBuilder<S> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
ArgumentBuilder::new(Argument::new(name, Arc::new(parser)).into())
|
||||
}
|
||||
|
|
241
azalea-brigadier/src/command_dispatcher.rs
Normal file → Executable file
241
azalea-brigadier/src/command_dispatcher.rs
Normal file → Executable file
|
@ -1,23 +1,21 @@
|
|||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, HashSet},
|
||||
ptr,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::{
|
||||
builder::argument_builder::ArgumentBuilder,
|
||||
context::{CommandContextBuilder, ContextChain},
|
||||
errors::{BuiltInError, CommandSyntaxError},
|
||||
context::{CommandContext, CommandContextBuilder},
|
||||
exceptions::{BuiltInExceptions, CommandSyntaxException},
|
||||
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.
|
||||
///
|
||||
|
@ -31,14 +29,12 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,7 +51,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());
|
||||
|
@ -67,10 +63,9 @@ impl<S> CommandDispatcher<S> {
|
|||
node: &Arc<RwLock<CommandNode<S>>>,
|
||||
original_reader: &StringReader,
|
||||
context_so_far: CommandContextBuilder<'a, S>,
|
||||
) -> Result<ParseResults<'a, S>, CommandSyntaxError> {
|
||||
) -> Result<ParseResults<'a, S>, CommandSyntaxException> {
|
||||
let source = context_so_far.source.clone();
|
||||
#[allow(clippy::mutable_key_type)] // this is fine because we don't mutate the key
|
||||
let mut errors = HashMap::<Rc<CommandNode<S>>, CommandSyntaxError>::new();
|
||||
let mut errors = HashMap::<Rc<CommandNode<S>>, CommandSyntaxException>::new();
|
||||
let mut potentials: Vec<ParseResults<S>> = vec![];
|
||||
let cursor = original_reader.cursor();
|
||||
|
||||
|
@ -86,7 +81,7 @@ impl<S> CommandDispatcher<S> {
|
|||
if let Err(ex) = parse_with_context_result {
|
||||
errors.insert(
|
||||
Rc::new((*child.read()).clone()),
|
||||
BuiltInError::DispatcherParseException {
|
||||
BuiltInExceptions::DispatcherParseException {
|
||||
message: ex.message(),
|
||||
}
|
||||
.create_with_context(&reader),
|
||||
|
@ -97,7 +92,8 @@ impl<S> CommandDispatcher<S> {
|
|||
if reader.can_read() && reader.peek() != ' ' {
|
||||
errors.insert(
|
||||
Rc::new((*child.read()).clone()),
|
||||
BuiltInError::DispatcherExpectedArgumentSeparator.create_with_context(&reader),
|
||||
BuiltInExceptions::DispatcherExpectedArgumentSeparator
|
||||
.create_with_context(&reader),
|
||||
);
|
||||
reader.cursor = cursor;
|
||||
continue;
|
||||
|
@ -110,30 +106,23 @@ impl<S> CommandDispatcher<S> {
|
|||
1
|
||||
}) {
|
||||
reader.skip();
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
potentials.push(ParseResults {
|
||||
|
@ -181,11 +170,11 @@ impl<S> CommandDispatcher<S> {
|
|||
&self,
|
||||
input: impl Into<StringReader>,
|
||||
source: S,
|
||||
) -> Result<i32, CommandSyntaxError> {
|
||||
) -> Result<i32, CommandSyntaxException> {
|
||||
let input = input.into();
|
||||
|
||||
let parse = self.parse(input, source);
|
||||
self.execute_parsed(parse)
|
||||
Self::execute_parsed(parse)
|
||||
}
|
||||
|
||||
pub fn add_paths(
|
||||
|
@ -224,39 +213,96 @@ 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 {
|
||||
match node.clone().read().child(name) {
|
||||
Some(child) => {
|
||||
node = child;
|
||||
}
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if let Some(child) = node.clone().read().child(name) {
|
||||
node = child;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(node)
|
||||
}
|
||||
|
||||
/// Executes a given pre-parsed command.
|
||||
pub fn execute_parsed(&self, parse: ParseResults<S>) -> Result<i32, CommandSyntaxError> {
|
||||
pub fn execute_parsed(parse: ParseResults<S>) -> Result<i32, CommandSyntaxException> {
|
||||
if parse.reader.can_read() {
|
||||
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)
|
||||
});
|
||||
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)
|
||||
);
|
||||
}
|
||||
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![];
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
let command = parse.reader.string();
|
||||
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));
|
||||
};
|
||||
if !found_command {
|
||||
// consumer.on_command_complete(original, false, 0);
|
||||
return Err(
|
||||
BuiltInExceptions::DispatcherUnknownCommand.create_with_context(&parse.reader)
|
||||
);
|
||||
}
|
||||
|
||||
flat_context.execute_all(original.source.clone(), self.consumer.as_ref())
|
||||
// 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 })
|
||||
}
|
||||
|
||||
pub fn get_all_usage(
|
||||
|
@ -284,35 +330,32 @@ impl<S> CommandDispatcher<S> {
|
|||
if node.command.is_some() {
|
||||
result.push(prefix.to_owned());
|
||||
}
|
||||
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}"));
|
||||
}
|
||||
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}"));
|
||||
}
|
||||
_ => {
|
||||
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,
|
||||
);
|
||||
}
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -364,7 +407,7 @@ impl<S> CommandDispatcher<S> {
|
|||
}
|
||||
|
||||
if let Some(redirect) = &node.redirect {
|
||||
let redirect = if ptr::eq(redirect.data_ptr(), self.root.data_ptr()) {
|
||||
let redirect = if redirect.data_ptr() == self.root.data_ptr() {
|
||||
"...".to_string()
|
||||
} else {
|
||||
format!("-> {}", redirect.read().usage_text())
|
||||
|
@ -404,7 +447,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
Normal file → Executable file
79
azalea-brigadier/src/context/command_context.rs
Normal file → Executable file
|
@ -1,31 +1,24 @@
|
|||
use std::{
|
||||
any::Any,
|
||||
collections::HashMap,
|
||||
fmt::{self, Debug},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use super::{ParsedArgument, parsed_command_node::ParsedCommandNode, string_range::StringRange};
|
||||
use super::{parsed_command_node::ParsedCommandNode, string_range::StringRange, ParsedArgument};
|
||||
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(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,
|
||||
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,
|
||||
}
|
||||
|
||||
impl<S> Clone for CommandContext<S> {
|
||||
|
@ -46,7 +39,7 @@ impl<S> Clone for CommandContext<S> {
|
|||
}
|
||||
|
||||
impl<S> Debug for CommandContext<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CommandContext")
|
||||
// .field("source", &self.source)
|
||||
.field("input", &self.input)
|
||||
|
@ -65,10 +58,8 @@ 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(),
|
||||
|
@ -83,52 +74,12 @@ 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 is_forked(&self) -> bool {
|
||||
self.forks
|
||||
pub fn argument(&self, name: &str) -> Option<Arc<dyn Any>> {
|
||||
let argument = self.arguments.get(name);
|
||||
argument.map(|a| a.result.clone())
|
||||
}
|
||||
}
|
||||
|
|
42
azalea-brigadier/src/context/command_context_builder.rs
Normal file → Executable file
42
azalea-brigadier/src/context/command_context_builder.rs
Normal file → Executable file
|
@ -1,21 +1,15 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Debug},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use super::{
|
||||
ParsedArgument, command_context::CommandContext, parsed_command_node::ParsedCommandNode,
|
||||
string_range::StringRange, suggestion_context::SuggestionContext,
|
||||
command_context::CommandContext, parsed_command_node::ParsedCommandNode,
|
||||
string_range::StringRange, suggestion_context::SuggestionContext, ParsedArgument,
|
||||
};
|
||||
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>,
|
||||
|
@ -69,7 +63,7 @@ impl<'a, S> CommandContextBuilder<'a, S> {
|
|||
}
|
||||
|
||||
pub fn with_command(&mut self, command: &Command<S>) -> &Self {
|
||||
self.command.clone_from(command);
|
||||
self.command = command.clone();
|
||||
self
|
||||
}
|
||||
pub fn with_child(&mut self, child: Rc<CommandContextBuilder<'a, S>>) -> &Self {
|
||||
|
@ -86,7 +80,7 @@ impl<'a, S> CommandContextBuilder<'a, S> {
|
|||
range,
|
||||
});
|
||||
self.range = StringRange::encompassing(&self.range, &range);
|
||||
self.modifier.clone_from(&node.read().modifier);
|
||||
self.modifier = node.read().modifier.clone();
|
||||
self.forks = node.read().forks;
|
||||
self
|
||||
}
|
||||
|
@ -112,18 +106,18 @@ impl<'a, S> CommandContextBuilder<'a, S> {
|
|||
}
|
||||
|
||||
if self.range.end() < cursor {
|
||||
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(),
|
||||
},
|
||||
},
|
||||
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(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut prev = &self.root;
|
||||
|
@ -145,7 +139,7 @@ impl<'a, S> CommandContextBuilder<'a, S> {
|
|||
}
|
||||
|
||||
impl<S> Debug for CommandContextBuilder<'_, S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CommandContextBuilder")
|
||||
// .field("arguments", &self.arguments)
|
||||
.field("root", &self.root)
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
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
Normal file → Executable file
2
azalea-brigadier/src/context/mod.rs
Normal file → Executable file
|
@ -1,6 +1,5 @@
|
|||
mod command_context;
|
||||
mod command_context_builder;
|
||||
mod context_chain;
|
||||
mod parsed_argument;
|
||||
mod parsed_command_node;
|
||||
mod string_range;
|
||||
|
@ -8,7 +7,6 @@ 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
Normal file → Executable file
3
azalea-brigadier/src/context/parsed_argument.rs
Normal file → Executable file
|
@ -1,6 +1,5 @@
|
|||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use super::string_range::StringRange;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ParsedArgument {
|
||||
|
|
3
azalea-brigadier/src/context/parsed_command_node.rs
Normal file → Executable file
3
azalea-brigadier/src/context/parsed_command_node.rs
Normal file → Executable file
|
@ -1,9 +1,8 @@
|
|||
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
Normal file → Executable file
0
azalea-brigadier/src/context/string_range.rs
Normal file → Executable file
|
@ -1,93 +0,0 @@
|
|||
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())
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
mod builtin_errors;
|
||||
mod command_syntax_error;
|
||||
|
||||
pub use builtin_errors::BuiltInError;
|
||||
pub use command_syntax_error::CommandSyntaxError;
|
71
azalea-brigadier/src/errors/builtin_errors.rs → azalea-brigadier/src/exceptions/builtin_exceptions.rs
Normal file → Executable file
71
azalea-brigadier/src/errors/builtin_errors.rs → azalea-brigadier/src/exceptions/builtin_exceptions.rs
Normal file → Executable file
|
@ -1,10 +1,11 @@
|
|||
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 BuiltInError {
|
||||
pub enum BuiltInExceptions {
|
||||
DoubleTooSmall { found: f64, min: f64 },
|
||||
DoubleTooBig { found: f64, max: f64 },
|
||||
|
||||
|
@ -40,114 +41,114 @@ pub enum BuiltInError {
|
|||
DispatcherParseException { message: String },
|
||||
}
|
||||
|
||||
impl fmt::Debug for BuiltInError {
|
||||
impl fmt::Debug for BuiltInExceptions {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
BuiltInError::DoubleTooSmall { found, min } => {
|
||||
BuiltInExceptions::DoubleTooSmall { found, min } => {
|
||||
write!(f, "Double must not be less than {min}, found {found}")
|
||||
}
|
||||
BuiltInError::DoubleTooBig { found, max } => {
|
||||
BuiltInExceptions::DoubleTooBig { found, max } => {
|
||||
write!(f, "Double must not be more than {max}, found {found}")
|
||||
}
|
||||
|
||||
BuiltInError::FloatTooSmall { found, min } => {
|
||||
BuiltInExceptions::FloatTooSmall { found, min } => {
|
||||
write!(f, "Float must not be less than {min}, found {found}")
|
||||
}
|
||||
BuiltInError::FloatTooBig { found, max } => {
|
||||
BuiltInExceptions::FloatTooBig { found, max } => {
|
||||
write!(f, "Float must not be more than {max}, found {found}")
|
||||
}
|
||||
|
||||
BuiltInError::IntegerTooSmall { found, min } => {
|
||||
BuiltInExceptions::IntegerTooSmall { found, min } => {
|
||||
write!(f, "Integer must not be less than {min}, found {found}")
|
||||
}
|
||||
BuiltInError::IntegerTooBig { found, max } => {
|
||||
BuiltInExceptions::IntegerTooBig { found, max } => {
|
||||
write!(f, "Integer must not be more than {max}, found {found}")
|
||||
}
|
||||
|
||||
BuiltInError::LongTooSmall { found, min } => {
|
||||
BuiltInExceptions::LongTooSmall { found, min } => {
|
||||
write!(f, "Long must not be less than {min}, found {found}")
|
||||
}
|
||||
BuiltInError::LongTooBig { found, max } => {
|
||||
BuiltInExceptions::LongTooBig { found, max } => {
|
||||
write!(f, "Long must not be more than {max}, found {found}")
|
||||
}
|
||||
|
||||
BuiltInError::LiteralIncorrect { expected } => {
|
||||
BuiltInExceptions::LiteralIncorrect { expected } => {
|
||||
write!(f, "Expected literal {expected}")
|
||||
}
|
||||
|
||||
BuiltInError::ReaderExpectedStartOfQuote => {
|
||||
BuiltInExceptions::ReaderExpectedStartOfQuote => {
|
||||
write!(f, "Expected quote to start a string")
|
||||
}
|
||||
BuiltInError::ReaderExpectedEndOfQuote => {
|
||||
BuiltInExceptions::ReaderExpectedEndOfQuote => {
|
||||
write!(f, "Unclosed quoted string")
|
||||
}
|
||||
BuiltInError::ReaderInvalidEscape { character } => {
|
||||
BuiltInExceptions::ReaderInvalidEscape { character } => {
|
||||
write!(f, "Invalid escape sequence '{character}' in quoted string")
|
||||
}
|
||||
BuiltInError::ReaderInvalidBool { value } => {
|
||||
BuiltInExceptions::ReaderInvalidBool { value } => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid bool, expected true or false but found '{value}'"
|
||||
)
|
||||
}
|
||||
BuiltInError::ReaderInvalidInt { value } => {
|
||||
BuiltInExceptions::ReaderInvalidInt { value } => {
|
||||
write!(f, "Invalid Integer '{value}'")
|
||||
}
|
||||
BuiltInError::ReaderExpectedInt => {
|
||||
BuiltInExceptions::ReaderExpectedInt => {
|
||||
write!(f, "Expected Integer")
|
||||
}
|
||||
BuiltInError::ReaderInvalidLong { value } => {
|
||||
BuiltInExceptions::ReaderInvalidLong { value } => {
|
||||
write!(f, "Invalid long '{value}'")
|
||||
}
|
||||
BuiltInError::ReaderExpectedLong => {
|
||||
BuiltInExceptions::ReaderExpectedLong => {
|
||||
write!(f, "Expected long")
|
||||
}
|
||||
BuiltInError::ReaderInvalidDouble { value } => {
|
||||
BuiltInExceptions::ReaderInvalidDouble { value } => {
|
||||
write!(f, "Invalid double '{value}'")
|
||||
}
|
||||
BuiltInError::ReaderExpectedDouble => {
|
||||
BuiltInExceptions::ReaderExpectedDouble => {
|
||||
write!(f, "Expected double")
|
||||
}
|
||||
BuiltInError::ReaderInvalidFloat { value } => {
|
||||
BuiltInExceptions::ReaderInvalidFloat { value } => {
|
||||
write!(f, "Invalid Float '{value}'")
|
||||
}
|
||||
BuiltInError::ReaderExpectedFloat => {
|
||||
BuiltInExceptions::ReaderExpectedFloat => {
|
||||
write!(f, "Expected Float")
|
||||
}
|
||||
BuiltInError::ReaderExpectedBool => {
|
||||
BuiltInExceptions::ReaderExpectedBool => {
|
||||
write!(f, "Expected bool")
|
||||
}
|
||||
BuiltInError::ReaderExpectedSymbol { symbol } => {
|
||||
BuiltInExceptions::ReaderExpectedSymbol { symbol } => {
|
||||
write!(f, "Expected '{symbol}'")
|
||||
}
|
||||
|
||||
BuiltInError::DispatcherUnknownCommand => {
|
||||
BuiltInExceptions::DispatcherUnknownCommand => {
|
||||
write!(f, "Unknown command")
|
||||
}
|
||||
BuiltInError::DispatcherUnknownArgument => {
|
||||
BuiltInExceptions::DispatcherUnknownArgument => {
|
||||
write!(f, "Incorrect argument for command")
|
||||
}
|
||||
BuiltInError::DispatcherExpectedArgumentSeparator => {
|
||||
BuiltInExceptions::DispatcherExpectedArgumentSeparator => {
|
||||
write!(
|
||||
f,
|
||||
"Expected whitespace to end one argument, but found trailing data"
|
||||
)
|
||||
}
|
||||
BuiltInError::DispatcherParseException { message } => {
|
||||
BuiltInExceptions::DispatcherParseException { message } => {
|
||||
write!(f, "Could not parse command: {message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BuiltInError {
|
||||
pub fn create(self) -> CommandSyntaxError {
|
||||
impl BuiltInExceptions {
|
||||
pub fn create(self) -> CommandSyntaxException {
|
||||
let message = format!("{self:?}");
|
||||
CommandSyntaxError::create(self, message)
|
||||
CommandSyntaxException::create(self, message)
|
||||
}
|
||||
|
||||
pub fn create_with_context(self, reader: &StringReader) -> CommandSyntaxError {
|
||||
pub fn create_with_context(self, reader: &StringReader) -> CommandSyntaxException {
|
||||
let message = format!("{self:?}");
|
||||
CommandSyntaxError::new(self, message, reader.string(), reader.cursor())
|
||||
CommandSyntaxException::new(self, message, reader.string(), reader.cursor())
|
||||
}
|
||||
}
|
94
azalea-brigadier/src/exceptions/command_syntax_exception.rs
Executable file
94
azalea-brigadier/src/exceptions/command_syntax_exception.rs
Executable file
|
@ -0,0 +1,94 @@
|
|||
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())
|
||||
}
|
||||
}
|
5
azalea-brigadier/src/exceptions/mod.rs
Executable file
5
azalea-brigadier/src/exceptions/mod.rs
Executable file
|
@ -0,0 +1,5 @@
|
|||
mod builtin_exceptions;
|
||||
mod command_syntax_exception;
|
||||
|
||||
pub use builtin_exceptions::BuiltInExceptions;
|
||||
pub use command_syntax_exception::CommandSyntaxException;
|
3
azalea-brigadier/src/lib.rs
Normal file → Executable file
3
azalea-brigadier/src/lib.rs
Normal file → Executable file
|
@ -4,10 +4,9 @@ pub mod arguments;
|
|||
pub mod builder;
|
||||
pub mod command_dispatcher;
|
||||
pub mod context;
|
||||
pub mod errors;
|
||||
pub mod exceptions;
|
||||
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
Normal file → Executable file
4
azalea-brigadier/src/modifier.rs
Normal file → Executable file
|
@ -1,6 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{context::CommandContext, errors::CommandSyntaxError};
|
||||
use crate::{context::CommandContext, exceptions::CommandSyntaxException};
|
||||
|
||||
pub type RedirectModifier<S> =
|
||||
dyn Fn(&CommandContext<S>) -> Result<Vec<Arc<S>>, CommandSyntaxError> + Send + Sync;
|
||||
dyn Fn(&CommandContext<S>) -> Result<Vec<Arc<S>>, CommandSyntaxException> + Send + Sync;
|
||||
|
|
15
azalea-brigadier/src/parse_results.rs
Normal file → Executable file
15
azalea-brigadier/src/parse_results.rs
Normal file → Executable file
|
@ -1,22 +1,17 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Debug},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
context::CommandContextBuilder, errors::CommandSyntaxError, string_reader::StringReader,
|
||||
tree::CommandNode,
|
||||
context::CommandContextBuilder, exceptions::CommandSyntaxException,
|
||||
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>>, CommandSyntaxError>,
|
||||
pub exceptions: HashMap<Rc<CommandNode<S>>, CommandSyntaxException>,
|
||||
}
|
||||
|
||||
impl<S> Debug for ParseResults<'_, S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ParseResults")
|
||||
.field("context", &self.context)
|
||||
// .field("reader", &self.reader)
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
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
Normal file → Executable file
54
azalea-brigadier/src/string_reader.rs
Normal file → Executable file
|
@ -1,7 +1,6 @@
|
|||
use crate::exceptions::{BuiltInExceptions, CommandSyntaxException};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::errors::{BuiltInError, CommandSyntaxError};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StringReader {
|
||||
string: String,
|
||||
|
@ -91,19 +90,19 @@ impl StringReader {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn read_int(&mut self) -> Result<i32, CommandSyntaxError> {
|
||||
pub fn read_int(&mut self) -> Result<i32, CommandSyntaxException> {
|
||||
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(BuiltInError::ReaderExpectedInt.create_with_context(self));
|
||||
return Err(BuiltInExceptions::ReaderExpectedInt.create_with_context(self));
|
||||
}
|
||||
let result = i32::from_str(number);
|
||||
if result.is_err() {
|
||||
self.cursor = start;
|
||||
return Err(BuiltInError::ReaderInvalidInt {
|
||||
return Err(BuiltInExceptions::ReaderInvalidInt {
|
||||
value: number.to_string(),
|
||||
}
|
||||
.create_with_context(self));
|
||||
|
@ -112,19 +111,19 @@ impl StringReader {
|
|||
Ok(result.unwrap())
|
||||
}
|
||||
|
||||
pub fn read_long(&mut self) -> Result<i64, CommandSyntaxError> {
|
||||
pub fn read_long(&mut self) -> Result<i64, CommandSyntaxException> {
|
||||
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(BuiltInError::ReaderExpectedLong.create_with_context(self));
|
||||
return Err(BuiltInExceptions::ReaderExpectedLong.create_with_context(self));
|
||||
}
|
||||
let result = i64::from_str(number);
|
||||
if result.is_err() {
|
||||
self.cursor = start;
|
||||
return Err(BuiltInError::ReaderInvalidLong {
|
||||
return Err(BuiltInExceptions::ReaderInvalidLong {
|
||||
value: number.to_string(),
|
||||
}
|
||||
.create_with_context(self));
|
||||
|
@ -133,19 +132,19 @@ impl StringReader {
|
|||
Ok(result.unwrap())
|
||||
}
|
||||
|
||||
pub fn read_double(&mut self) -> Result<f64, CommandSyntaxError> {
|
||||
pub fn read_double(&mut self) -> Result<f64, CommandSyntaxException> {
|
||||
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(BuiltInError::ReaderExpectedDouble.create_with_context(self));
|
||||
return Err(BuiltInExceptions::ReaderExpectedDouble.create_with_context(self));
|
||||
}
|
||||
let result = f64::from_str(number);
|
||||
if result.is_err() {
|
||||
self.cursor = start;
|
||||
return Err(BuiltInError::ReaderInvalidDouble {
|
||||
return Err(BuiltInExceptions::ReaderInvalidDouble {
|
||||
value: number.to_string(),
|
||||
}
|
||||
.create_with_context(self));
|
||||
|
@ -154,19 +153,19 @@ impl StringReader {
|
|||
Ok(result.unwrap())
|
||||
}
|
||||
|
||||
pub fn read_float(&mut self) -> Result<f32, CommandSyntaxError> {
|
||||
pub fn read_float(&mut self) -> Result<f32, CommandSyntaxException> {
|
||||
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(BuiltInError::ReaderExpectedFloat.create_with_context(self));
|
||||
return Err(BuiltInExceptions::ReaderExpectedFloat.create_with_context(self));
|
||||
}
|
||||
let result = f32::from_str(number);
|
||||
if result.is_err() {
|
||||
self.cursor = start;
|
||||
return Err(BuiltInError::ReaderInvalidFloat {
|
||||
return Err(BuiltInExceptions::ReaderInvalidFloat {
|
||||
value: number.to_string(),
|
||||
}
|
||||
.create_with_context(self));
|
||||
|
@ -193,19 +192,22 @@ impl StringReader {
|
|||
&self.string[start..self.cursor]
|
||||
}
|
||||
|
||||
pub fn read_quoted_string(&mut self) -> Result<String, CommandSyntaxError> {
|
||||
pub fn read_quoted_string(&mut self) -> Result<String, CommandSyntaxException> {
|
||||
if !self.can_read() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
let next = self.peek();
|
||||
if !StringReader::is_quoted_string_start(next) {
|
||||
return Err(BuiltInError::ReaderExpectedStartOfQuote.create_with_context(self));
|
||||
return Err(BuiltInExceptions::ReaderExpectedStartOfQuote.create_with_context(self));
|
||||
}
|
||||
self.skip();
|
||||
self.read_string_until(next)
|
||||
}
|
||||
|
||||
pub fn read_string_until(&mut self, terminator: char) -> Result<String, CommandSyntaxError> {
|
||||
pub fn read_string_until(
|
||||
&mut self,
|
||||
terminator: char,
|
||||
) -> Result<String, CommandSyntaxException> {
|
||||
let mut result = String::new();
|
||||
let mut escaped = false;
|
||||
while self.can_read() {
|
||||
|
@ -216,7 +218,7 @@ impl StringReader {
|
|||
escaped = false;
|
||||
} else {
|
||||
self.cursor -= 1;
|
||||
return Err(BuiltInError::ReaderInvalidEscape { character: c }
|
||||
return Err(BuiltInExceptions::ReaderInvalidEscape { character: c }
|
||||
.create_with_context(self));
|
||||
}
|
||||
} else if c == SYNTAX_ESCAPE {
|
||||
|
@ -228,10 +230,10 @@ impl StringReader {
|
|||
}
|
||||
}
|
||||
|
||||
Err(BuiltInError::ReaderExpectedEndOfQuote.create_with_context(self))
|
||||
Err(BuiltInExceptions::ReaderExpectedEndOfQuote.create_with_context(self))
|
||||
}
|
||||
|
||||
pub fn read_string(&mut self) -> Result<String, CommandSyntaxError> {
|
||||
pub fn read_string(&mut self) -> Result<String, CommandSyntaxException> {
|
||||
if !self.can_read() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
@ -243,11 +245,11 @@ impl StringReader {
|
|||
Ok(self.read_unquoted_string().to_string())
|
||||
}
|
||||
|
||||
pub fn read_boolean(&mut self) -> Result<bool, CommandSyntaxError> {
|
||||
pub fn read_boolean(&mut self) -> Result<bool, CommandSyntaxException> {
|
||||
let start = self.cursor;
|
||||
let value = self.read_string()?;
|
||||
if value.is_empty() {
|
||||
return Err(BuiltInError::ReaderExpectedBool.create_with_context(self));
|
||||
return Err(BuiltInExceptions::ReaderExpectedBool.create_with_context(self));
|
||||
}
|
||||
|
||||
if value == "true" {
|
||||
|
@ -256,13 +258,15 @@ impl StringReader {
|
|||
Ok(false)
|
||||
} else {
|
||||
self.cursor = start;
|
||||
Err(BuiltInError::ReaderInvalidBool { value }.create_with_context(self))
|
||||
Err(BuiltInExceptions::ReaderInvalidBool { value }.create_with_context(self))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect(&mut self, c: char) -> Result<(), CommandSyntaxError> {
|
||||
pub fn expect(&mut self, c: char) -> Result<(), CommandSyntaxException> {
|
||||
if !self.can_read() || self.peek() != c {
|
||||
return Err(BuiltInError::ReaderExpectedSymbol { symbol: c }.create_with_context(self));
|
||||
return Err(
|
||||
BuiltInExceptions::ReaderExpectedSymbol { symbol: c }.create_with_context(self)
|
||||
);
|
||||
}
|
||||
self.skip();
|
||||
Ok(())
|
||||
|
|
31
azalea-brigadier/src/suggestion/mod.rs
Normal file → Executable file
31
azalea-brigadier/src/suggestion/mod.rs
Normal file → Executable file
|
@ -1,25 +1,20 @@
|
|||
mod suggestion_provider;
|
||||
mod suggestions;
|
||||
mod suggestions_builder;
|
||||
|
||||
use crate::context::StringRange;
|
||||
#[cfg(feature = "azalea-buf")]
|
||||
use std::io::{self, Write};
|
||||
use azalea_buf::McBufWritable;
|
||||
#[cfg(feature = "azalea-buf")]
|
||||
use azalea_chat::FormattedText;
|
||||
#[cfg(feature = "azalea-buf")]
|
||||
use std::io::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
|
||||
|
@ -96,7 +91,7 @@ impl Suggestion {
|
|||
}
|
||||
|
||||
impl SuggestionValue {
|
||||
pub fn cmp_ignore_case(&self, other: &Self) -> Ordering {
|
||||
pub fn cmp_ignore_case(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match (self, other) {
|
||||
(SuggestionValue::Text(a), SuggestionValue::Text(b)) => {
|
||||
a.to_lowercase().cmp(&b.to_lowercase())
|
||||
|
@ -121,7 +116,7 @@ impl Display for SuggestionValue {
|
|||
}
|
||||
|
||||
impl Ord for SuggestionValue {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match (self, other) {
|
||||
(SuggestionValue::Text(a), SuggestionValue::Text(b)) => a.cmp(b),
|
||||
(SuggestionValue::Integer(a), SuggestionValue::Integer(b)) => a.cmp(b),
|
||||
|
@ -134,19 +129,19 @@ impl Ord for SuggestionValue {
|
|||
}
|
||||
}
|
||||
impl PartialOrd for SuggestionValue {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "azalea-buf")]
|
||||
impl AzaleaWrite for Suggestion {
|
||||
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
|
||||
self.value.to_string().azalea_write(buf)?;
|
||||
impl McBufWritable for Suggestion {
|
||||
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
|
||||
self.value.to_string().write_into(buf)?;
|
||||
self.tooltip
|
||||
.clone()
|
||||
.map(FormattedText::from)
|
||||
.azalea_write(buf)?;
|
||||
.write_into(buf)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
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
Normal file → Executable file
42
azalea-brigadier/src/suggestion/suggestions.rs
Normal file → Executable 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 AzaleaRead for Suggestions {
|
||||
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
|
||||
#[derive(AzBuf)]
|
||||
impl McBufReadable for Suggestions {
|
||||
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
|
||||
#[derive(McBuf)]
|
||||
struct StandaloneSuggestion {
|
||||
pub text: String,
|
||||
pub tooltip: Option<FormattedText>,
|
||||
}
|
||||
|
||||
let start = u32::azalea_read_var(buf)? as usize;
|
||||
let length = u32::azalea_read_var(buf)? as usize;
|
||||
let start = u32::var_read_from(buf)? as usize;
|
||||
let length = u32::var_read_from(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 AzaleaRead for Suggestion`
|
||||
let mut suggestions = Vec::<StandaloneSuggestion>::azalea_read(buf)?
|
||||
// so we can't just `impl McBufReadable for Suggestion`
|
||||
let mut suggestions = Vec::<StandaloneSuggestion>::read_from(buf)?
|
||||
.into_iter()
|
||||
.map(|s| Suggestion {
|
||||
value: SuggestionValue::Text(s.text),
|
||||
|
@ -106,11 +106,11 @@ impl AzaleaRead for Suggestions {
|
|||
}
|
||||
|
||||
#[cfg(feature = "azalea-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)?;
|
||||
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)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
3
azalea-brigadier/src/suggestion/suggestions_builder.rs
Normal file → Executable file
3
azalea-brigadier/src/suggestion/suggestions_builder.rs
Normal file → Executable file
|
@ -1,8 +1,9 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use super::{Suggestion, SuggestionValue, Suggestions};
|
||||
use crate::context::StringRange;
|
||||
|
||||
use super::{Suggestion, SuggestionValue, Suggestions};
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct SuggestionsBuilder {
|
||||
input: String,
|
||||
|
|
67
azalea-brigadier/src/tree/mod.rs
Normal file → Executable file
67
azalea-brigadier/src/tree/mod.rs
Normal file → Executable file
|
@ -1,11 +1,3 @@
|
|||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::{self, Debug},
|
||||
hash::{Hash, Hasher},
|
||||
ptr,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::{
|
||||
|
@ -14,19 +6,25 @@ use crate::{
|
|||
required_argument_builder::Argument,
|
||||
},
|
||||
context::{CommandContext, CommandContextBuilder, ParsedArgument, StringRange},
|
||||
errors::{BuiltInError, CommandSyntaxError},
|
||||
exceptions::{BuiltInExceptions, CommandSyntaxException},
|
||||
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>) -> Result<i32, CommandSyntaxError> + Send + Sync>>;
|
||||
pub type Command<S> = Option<Arc<dyn Fn(&CommandContext<S>) -> i32 + Send + Sync>>;
|
||||
|
||||
/// An ArgumentBuilder that has been built.
|
||||
#[non_exhaustive]
|
||||
pub struct CommandNode<S> {
|
||||
pub value: ArgumentBuilderType<S>,
|
||||
pub value: ArgumentBuilderType,
|
||||
|
||||
// this is a BTreeMap because children need to be ordered when getting command suggestions
|
||||
pub children: BTreeMap<String, Arc<RwLock<CommandNode<S>>>>,
|
||||
|
@ -67,7 +65,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<S> {
|
||||
pub fn argument(&self) -> &Argument {
|
||||
match self.value {
|
||||
ArgumentBuilderType::Argument(ref argument) => argument,
|
||||
_ => panic!("CommandNode::argument() called on non-argument node"),
|
||||
|
@ -150,7 +148,7 @@ impl<S> CommandNode<S> {
|
|||
&self,
|
||||
reader: &mut StringReader,
|
||||
context_builder: &mut CommandContextBuilder<S>,
|
||||
) -> Result<(), CommandSyntaxError> {
|
||||
) -> Result<(), CommandSyntaxException> {
|
||||
match self.value {
|
||||
ArgumentBuilderType::Argument(ref argument) => {
|
||||
let start = reader.cursor();
|
||||
|
@ -177,7 +175,7 @@ impl<S> CommandNode<S> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
Err(BuiltInError::LiteralIncorrect {
|
||||
Err(BuiltInExceptions::LiteralIncorrect {
|
||||
expected: literal.value.clone(),
|
||||
}
|
||||
.create_with_context(reader))
|
||||
|
@ -215,7 +213,9 @@ impl<S> CommandNode<S> {
|
|||
|
||||
pub fn list_suggestions(
|
||||
&self,
|
||||
context: CommandContext<S>,
|
||||
// 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>,
|
||||
builder: SuggestionsBuilder,
|
||||
) -> Suggestions {
|
||||
match &self.value {
|
||||
|
@ -230,15 +230,15 @@ impl<S> CommandNode<S> {
|
|||
Suggestions::default()
|
||||
}
|
||||
}
|
||||
ArgumentBuilderType::Argument(argument) => argument.list_suggestions(context, builder),
|
||||
ArgumentBuilderType::Argument(argument) => argument.list_suggestions(builder),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Debug for CommandNode<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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: Hasher>(&self, state: &mut H) {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
// hash the children
|
||||
for (k, v) in &self.children {
|
||||
k.hash(state);
|
||||
|
@ -291,27 +291,18 @@ impl<S> PartialEq for CommandNode<S> {
|
|||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if other.command.is_some() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
0
azalea-brigadier/tests/arguments/bool_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/bool_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/double_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/double_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/float_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/float_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/integer_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/integer_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/long_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/long_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/string_argument_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/arguments/string_argument_type_test.rs
Normal file → Executable file
|
@ -1,189 +0,0 @@
|
|||
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
Normal file → Executable file
12
azalea-brigadier/tests/builder/argument_builder_test.rs
Normal file → Executable file
|
@ -10,13 +10,11 @@ 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
Normal file → Executable file
0
azalea-brigadier/tests/builder/literal_argument_builder_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/builder/required_argument_builder_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/builder/required_argument_builder_test.rs
Normal file → Executable file
54
azalea-brigadier/tests/command_dispatcher_test.rs
Normal file → Executable file
54
azalea-brigadier/tests/command_dispatcher_test.rs
Normal file → Executable file
|
@ -5,7 +5,7 @@ use azalea_brigadier::{
|
|||
builder::{literal_argument_builder::literal, required_argument_builder::argument},
|
||||
command_dispatcher::CommandDispatcher,
|
||||
context::CommandContext,
|
||||
errors::{BuiltInError, CommandSyntaxError},
|
||||
exceptions::{BuiltInExceptions, CommandSyntaxException},
|
||||
string_reader::StringReader,
|
||||
};
|
||||
|
||||
|
@ -50,7 +50,10 @@ fn execute_unknown_command() {
|
|||
let execute_result = subject.execute("foo", &CommandSource {});
|
||||
|
||||
let err = execute_result.err().unwrap();
|
||||
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownCommand);
|
||||
match err.type_ {
|
||||
BuiltInExceptions::DispatcherUnknownCommand => {}
|
||||
_ => panic!("Unexpected error"),
|
||||
}
|
||||
assert_eq!(err.cursor().unwrap(), 0);
|
||||
}
|
||||
|
||||
|
@ -62,7 +65,10 @@ fn execute_impermissible_command() {
|
|||
let execute_result = subject.execute("foo", &CommandSource {});
|
||||
|
||||
let err = execute_result.err().unwrap();
|
||||
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownCommand);
|
||||
match err.type_ {
|
||||
BuiltInExceptions::DispatcherUnknownCommand => {}
|
||||
_ => panic!("Unexpected error"),
|
||||
}
|
||||
assert_eq!(err.cursor().unwrap(), 0);
|
||||
}
|
||||
|
||||
|
@ -74,7 +80,10 @@ fn execute_empty_command() {
|
|||
let execute_result = subject.execute("", &CommandSource {});
|
||||
|
||||
let err = execute_result.err().unwrap();
|
||||
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownCommand);
|
||||
match err.type_ {
|
||||
BuiltInExceptions::DispatcherUnknownCommand => {}
|
||||
_ => panic!("Unexpected error"),
|
||||
}
|
||||
assert_eq!(err.cursor().unwrap(), 0);
|
||||
}
|
||||
|
||||
|
@ -86,7 +95,10 @@ fn execute_unknown_subcommand() {
|
|||
let execute_result = subject.execute("foo bar", &CommandSource {});
|
||||
|
||||
let err = execute_result.err().unwrap();
|
||||
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownArgument);
|
||||
match err.type_ {
|
||||
BuiltInExceptions::DispatcherUnknownArgument => {}
|
||||
_ => panic!("Unexpected error"),
|
||||
}
|
||||
assert_eq!(err.cursor().unwrap(), 4);
|
||||
}
|
||||
|
||||
|
@ -98,7 +110,10 @@ fn execute_incorrect_literal() {
|
|||
let execute_result = subject.execute("foo baz", &CommandSource {});
|
||||
|
||||
let err = execute_result.err().unwrap();
|
||||
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownArgument);
|
||||
match err.type_ {
|
||||
BuiltInExceptions::DispatcherUnknownArgument => {}
|
||||
_ => panic!("Unexpected error"),
|
||||
}
|
||||
assert_eq!(err.cursor().unwrap(), 4);
|
||||
}
|
||||
|
||||
|
@ -115,7 +130,10 @@ fn execute_ambiguous_incorrect_argument() {
|
|||
let execute_result = subject.execute("foo unknown", &CommandSource {});
|
||||
|
||||
let err = execute_result.err().unwrap();
|
||||
assert_eq!(err.kind(), &BuiltInError::DispatcherUnknownArgument);
|
||||
match err.type_ {
|
||||
BuiltInExceptions::DispatcherUnknownArgument => {}
|
||||
_ => panic!("Unexpected error"),
|
||||
}
|
||||
assert_eq!(err.cursor().unwrap(), 4);
|
||||
}
|
||||
|
||||
|
@ -155,7 +173,7 @@ fn parse_incomplete_argument() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn execute_ambiguous_parent_subcommand() {
|
||||
fn execute_ambiguious_parent_subcommand() {
|
||||
let mut subject = CommandDispatcher::new();
|
||||
|
||||
subject.register(
|
||||
|
@ -168,7 +186,7 @@ fn execute_ambiguous_parent_subcommand() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn execute_ambiguous_parent_subcommand_via_redirect() {
|
||||
fn execute_ambiguious_parent_subcommand_via_redirect() {
|
||||
let mut subject = CommandDispatcher::new();
|
||||
|
||||
let real = subject.register(
|
||||
|
@ -227,7 +245,7 @@ fn execute_redirected_multiple_times() {
|
|||
);
|
||||
assert_eq!(*child2.unwrap().nodes[0].node.read(), *concrete_node.read());
|
||||
|
||||
assert_eq!(subject.execute_parsed(parse).unwrap(), 42);
|
||||
assert_eq!(CommandDispatcher::execute_parsed(parse).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -237,7 +255,7 @@ fn execute_redirected() {
|
|||
let source1 = Arc::new(CommandSource {});
|
||||
let source2 = Arc::new(CommandSource {});
|
||||
|
||||
let modifier = move |_: &CommandContext<CommandSource>| -> Result<Vec<Arc<CommandSource>>, CommandSyntaxError> {
|
||||
let modifier = move |_: &CommandContext<CommandSource>| -> Result<Vec<Arc<CommandSource>>, CommandSyntaxException> {
|
||||
Ok(vec![source1.clone(), source2.clone()])
|
||||
};
|
||||
|
||||
|
@ -263,7 +281,7 @@ fn execute_redirected() {
|
|||
assert_eq!(*parent.nodes[0].node.read(), *concrete_node.read());
|
||||
assert_eq!(*parent.source, CommandSource {});
|
||||
|
||||
assert_eq!(subject.execute_parsed(parse).unwrap(), 2);
|
||||
assert_eq!(CommandDispatcher::execute_parsed(parse).unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -279,7 +297,10 @@ fn execute_orphaned_subcommand() {
|
|||
let result = subject.execute("foo 5", &CommandSource {});
|
||||
assert!(result.is_err());
|
||||
let result = result.unwrap_err();
|
||||
assert_eq!(*result.kind(), BuiltInError::DispatcherUnknownCommand);
|
||||
assert_eq!(
|
||||
*result.get_type(),
|
||||
BuiltInExceptions::DispatcherUnknownCommand
|
||||
);
|
||||
assert_eq!(result.cursor(), Some(5));
|
||||
}
|
||||
|
||||
|
@ -306,7 +327,10 @@ fn parse_no_space_separator() {
|
|||
let result = subject.execute("foo$", &CommandSource {});
|
||||
assert!(result.is_err());
|
||||
let result = result.unwrap_err();
|
||||
assert_eq!(*result.kind(), BuiltInError::DispatcherUnknownCommand);
|
||||
assert_eq!(
|
||||
*result.get_type(),
|
||||
BuiltInExceptions::DispatcherUnknownCommand
|
||||
);
|
||||
assert_eq!(result.cursor(), Some(0));
|
||||
}
|
||||
|
||||
|
@ -324,7 +348,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(), BuiltInError::ReaderExpectedInt);
|
||||
// assert_eq!(*result.get_type(), BuiltInExceptions::ReaderExpectedInt);
|
||||
assert_eq!(result.cursor(), Some(4));
|
||||
}
|
||||
|
||||
|
|
0
azalea-brigadier/tests/command_dispatcher_usages_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/command_dispatcher_usages_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/command_suggestions_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/command_suggestions_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/context/command_context_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/context/command_context_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/context/parsed_argument_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/context/parsed_argument_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs
Normal file → Executable file
52
azalea-brigadier/tests/string_reader_test.rs
Normal file → Executable file
52
azalea-brigadier/tests/string_reader_test.rs
Normal file → Executable file
|
@ -1,4 +1,4 @@
|
|||
use azalea_brigadier::{errors::BuiltInError, string_reader::StringReader};
|
||||
use azalea_brigadier::{exceptions::BuiltInExceptions, 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.kind(), &BuiltInError::ReaderExpectedStartOfQuote);
|
||||
assert_eq!(e.get_type(), &BuiltInExceptions::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.kind(), &BuiltInError::ReaderExpectedEndOfQuote);
|
||||
assert_eq!(e.get_type(), &BuiltInExceptions::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.kind(),
|
||||
&BuiltInError::ReaderInvalidEscape { character: 'n' }
|
||||
e.get_type(),
|
||||
&BuiltInExceptions::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.kind(),
|
||||
&BuiltInError::ReaderInvalidEscape { character: '"' }
|
||||
e.get_type(),
|
||||
&BuiltInExceptions::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.kind(),
|
||||
&BuiltInError::ReaderInvalidInt {
|
||||
e.get_type(),
|
||||
&BuiltInExceptions::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.kind(), &BuiltInError::ReaderExpectedInt);
|
||||
assert_eq!(e.get_type(), &BuiltInExceptions::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.kind(),
|
||||
&BuiltInError::ReaderInvalidLong {
|
||||
e.get_type(),
|
||||
&BuiltInExceptions::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.kind(), &BuiltInError::ReaderExpectedLong);
|
||||
assert_eq!(e.get_type(), &BuiltInExceptions::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.kind(),
|
||||
&BuiltInError::ReaderInvalidDouble {
|
||||
e.get_type(),
|
||||
&BuiltInExceptions::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.kind(), &BuiltInError::ReaderExpectedDouble);
|
||||
assert_eq!(e.get_type(), &BuiltInExceptions::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.kind(),
|
||||
&BuiltInError::ReaderInvalidFloat {
|
||||
e.get_type(),
|
||||
&BuiltInExceptions::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.kind(), &BuiltInError::ReaderExpectedFloat);
|
||||
assert_eq!(e.get_type(), &BuiltInExceptions::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.kind(),
|
||||
&BuiltInError::ReaderExpectedSymbol { symbol: 'a' }
|
||||
e.get_type(),
|
||||
&BuiltInExceptions::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.kind(),
|
||||
&BuiltInError::ReaderExpectedSymbol { symbol: 'a' }
|
||||
e.get_type(),
|
||||
&BuiltInExceptions::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.kind(),
|
||||
&BuiltInError::ReaderInvalidBool {
|
||||
e.get_type(),
|
||||
&BuiltInExceptions::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.kind(), &BuiltInError::ReaderExpectedBool);
|
||||
assert_eq!(e.get_type(), &BuiltInExceptions::ReaderExpectedBool);
|
||||
assert_eq!(e.cursor(), Some(0));
|
||||
}
|
||||
}
|
||||
|
|
0
azalea-brigadier/tests/suggestion/suggestion_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/suggestion/suggestion_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/suggestion/suggestions_builder_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/suggestion/suggestions_builder_test.rs
Normal file → Executable file
2
azalea-brigadier/tests/suggestion/suggestions_test.rs
Normal file → Executable file
2
azalea-brigadier/tests/suggestion/suggestions_test.rs
Normal file → Executable file
|
@ -17,7 +17,7 @@ fn merge_single() {
|
|||
StringRange::at(5),
|
||||
vec![Suggestion::new(StringRange::at(5), "ar")],
|
||||
);
|
||||
let merged = Suggestions::merge("foo b", std::slice::from_ref(&suggestions));
|
||||
let merged = Suggestions::merge("foo b", &[suggestions.clone()]);
|
||||
assert_eq!(merged, suggestions);
|
||||
}
|
||||
|
||||
|
|
0
azalea-brigadier/tests/tree/abstract_command_node_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/tree/abstract_command_node_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/tree/argument_command_node_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/tree/argument_command_node_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/tree/literal_command_node_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/tree/literal_command_node_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/tree/root_command_node_test.rs
Normal file → Executable file
0
azalea-brigadier/tests/tree/root_command_node_test.rs
Normal file → Executable file
|
@ -1,19 +1,21 @@
|
|||
[package]
|
||||
name = "azalea-buf"
|
||||
description = "Serialize and deserialize buffers from Minecraft."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
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
|
||||
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"
|
||||
|
||||
[features]
|
||||
serde_json = ["dep:serde_json"]
|
||||
|
|
0
azalea-buf/README.md
Normal file → Executable file
0
azalea-buf/README.md
Normal file → Executable file
|
@ -1,15 +1,16 @@
|
|||
[package]
|
||||
description = "#[derive(McBuf)]"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-buf-macros"
|
||||
description = "#[derive(AzBuf)]"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
repository = "https://github.com/azalea-rs/azalea/tree/main/azalea-buf"
|
||||
version = "0.9.1"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proc-macro2.workspace = true
|
||||
quote.workspace = true
|
||||
syn = { workspace = true, features = ["extra-traits"] }
|
||||
proc-macro2 = "^1.0.78"
|
||||
quote = "^1.0.35"
|
||||
syn = { version = "^2.0.49", features = ["extra-traits"] }
|
||||
|
|
0
azalea-buf/azalea-buf-macros/README.md
Normal file → Executable file
0
azalea-buf/azalea-buf-macros/README.md
Normal file → Executable file
22
azalea-buf/azalea-buf-macros/src/lib.rs
Normal file → Executable file
22
azalea-buf/azalea-buf-macros/src/lib.rs
Normal file → Executable file
|
@ -3,28 +3,28 @@ mod write;
|
|||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{DeriveInput, parse_macro_input};
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
#[proc_macro_derive(AzaleaRead, attributes(var))]
|
||||
pub fn derive_azalearead(input: TokenStream) -> TokenStream {
|
||||
#[proc_macro_derive(McBufReadable, attributes(var))]
|
||||
pub fn derive_mcbufreadable(input: TokenStream) -> TokenStream {
|
||||
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
|
||||
|
||||
read::create_impl_azalearead(&ident, &data).into()
|
||||
read::create_impl_mcbufreadable(&ident, &data).into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(AzaleaWrite, attributes(var))]
|
||||
pub fn derive_azaleawrite(input: TokenStream) -> TokenStream {
|
||||
#[proc_macro_derive(McBufWritable, attributes(var))]
|
||||
pub fn derive_mcbufwritable(input: TokenStream) -> TokenStream {
|
||||
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
|
||||
|
||||
write::create_impl_azaleawrite(&ident, &data).into()
|
||||
write::create_impl_mcbufwritable(&ident, &data).into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(AzBuf, attributes(var, limit))]
|
||||
pub fn derive_azbuf(input: TokenStream) -> TokenStream {
|
||||
#[proc_macro_derive(McBuf, attributes(var))]
|
||||
pub fn derive_mcbuf(input: TokenStream) -> TokenStream {
|
||||
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
|
||||
|
||||
let writable = write::create_impl_azaleawrite(&ident, &data);
|
||||
let readable = read::create_impl_azalearead(&ident, &data);
|
||||
let writable = write::create_impl_mcbufwritable(&ident, &data);
|
||||
let readable = read::create_impl_mcbufreadable(&ident, &data);
|
||||
quote! {
|
||||
#writable
|
||||
#readable
|
||||
|
|
|
@ -1,46 +1,61 @@
|
|||
use quote::{ToTokens, quote};
|
||||
use syn::{Data, Field, FieldsNamed, Ident, punctuated::Punctuated, token::Comma};
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{punctuated::Punctuated, token::Comma, Data, Field, FieldsNamed, Ident};
|
||||
|
||||
pub fn create_impl_azalearead(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => 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, .. }) => match fields {
|
||||
syn::Fields::Named(FieldsNamed { named, .. }) => {
|
||||
let (read_fields, read_field_names) = read_named_fields(named);
|
||||
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")
|
||||
};
|
||||
|
||||
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),*
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
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> {
|
||||
Ok(Self(
|
||||
#(#read_fields),*
|
||||
))
|
||||
}
|
||||
}
|
||||
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::Data::Enum(syn::DataEnum { variants, .. }) => {
|
||||
let mut match_contents = quote!();
|
||||
let mut variant_discrim: u32 = 0;
|
||||
|
@ -83,34 +98,13 @@ pub fn create_impl_azalearead(ident: &Ident, data: &Data) -> proc_macro2::TokenS
|
|||
syn::Fields::Unnamed(fields) => {
|
||||
let mut reader_code = quote! {};
|
||||
for f in &fields.unnamed {
|
||||
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 {
|
||||
if f.attrs.iter().any(|attr| attr.path().is_ident("var")) {
|
||||
reader_code.extend(quote! {
|
||||
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)?),
|
||||
Self::#variant_name(azalea_buf::McBufVarReadable::var_read_from(buf)?),
|
||||
});
|
||||
} else {
|
||||
reader_code.extend(quote! {
|
||||
Self::#variant_name(azalea_buf::AzaleaRead::azalea_read(buf)?),
|
||||
Self::#variant_name(azalea_buf::McBufReadable::read_from(buf)?),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -135,15 +129,15 @@ pub fn create_impl_azalearead(ident: &Ident, data: &Data) -> proc_macro2::TokenS
|
|||
let first_reader = first_reader.expect("There should be at least one variant");
|
||||
|
||||
quote! {
|
||||
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 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 #ident {
|
||||
pub fn azalea_read_id(buf: &mut std::io::Cursor<&[u8]>, id: u32) -> Result<Self, azalea_buf::BufReadError> {
|
||||
pub fn read_from_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
|
||||
|
@ -153,81 +147,6 @@ pub fn create_impl_azalearead(ident: &Ident, data: &Data) -> proc_macro2::TokenS
|
|||
}
|
||||
}
|
||||
}
|
||||
_ => 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()
|
||||
),
|
||||
_ => panic!("#[derive(McBuf)] can only be used on structs"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +1,61 @@
|
|||
use proc_macro2::Span;
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::{Data, Field, FieldsNamed, Ident, punctuated::Punctuated, token::Comma};
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{punctuated::Punctuated, token::Comma, Data, Field, FieldsNamed, Ident};
|
||||
|
||||
pub fn create_impl_azaleawrite(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
|
||||
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 {
|
||||
match data {
|
||||
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())));
|
||||
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")
|
||||
};
|
||||
|
||||
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);
|
||||
let write_fields =
|
||||
write_named_fields(named, Some(&Ident::new("self", Span::call_site())));
|
||||
|
||||
quote! {
|
||||
impl azalea_buf::AzaleaWrite for #ident {
|
||||
fn azalea_write(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
|
||||
#write_fields
|
||||
Ok(())
|
||||
}
|
||||
quote! {
|
||||
impl azalea_buf::McBufWritable for #ident {
|
||||
fn write_into(&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;
|
||||
|
@ -77,7 +93,7 @@ pub fn create_impl_azaleawrite(ident: &Ident, data: &Data) -> proc_macro2::Token
|
|||
|
||||
// the variant number that we're going to write
|
||||
let write_the_variant = quote! {
|
||||
azalea_buf::AzaleaWriteVar::azalea_write_var(&#variant_discrim, buf)?;
|
||||
azalea_buf::McBufVarWritable::var_write_into(&#variant_discrim, buf)?;
|
||||
};
|
||||
match &variant.fields {
|
||||
syn::Fields::Named(f) => {
|
||||
|
@ -119,11 +135,11 @@ pub fn create_impl_azaleawrite(ident: &Ident, data: &Data) -> proc_macro2::Token
|
|||
params_code.extend(quote! { #param_ident, });
|
||||
if f.attrs.iter().any(|attr| attr.path().is_ident("var")) {
|
||||
writers_code.extend(quote! {
|
||||
azalea_buf::AzaleaWriteVar::azalea_write_var(#param_ident, buf)?;
|
||||
azalea_buf::McBufVarWritable::var_write_into(#param_ident, buf)?;
|
||||
});
|
||||
} else {
|
||||
writers_code.extend(quote! {
|
||||
azalea_buf::AzaleaWrite::azalea_write(#param_ident, buf)?;
|
||||
azalea_buf::McBufWritable::write_into(#param_ident, buf)?;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +151,7 @@ pub fn create_impl_azaleawrite(ident: &Ident, data: &Data) -> proc_macro2::Token
|
|||
});
|
||||
match_arms_without_id.extend(quote! {
|
||||
Self::#variant_name(data) => {
|
||||
azalea_buf::AzaleaWrite::azalea_write(data, buf)?;
|
||||
azalea_buf::McBufWritable::write_into(data, buf)?;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -143,8 +159,8 @@ pub fn create_impl_azaleawrite(ident: &Ident, data: &Data) -> proc_macro2::Token
|
|||
}
|
||||
if is_data_enum {
|
||||
quote! {
|
||||
impl azalea_buf::AzaleaWrite for #ident {
|
||||
fn azalea_write(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
|
||||
impl azalea_buf::McBufWritable for #ident {
|
||||
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
|
||||
match self {
|
||||
#match_arms
|
||||
}
|
||||
|
@ -163,67 +179,14 @@ pub fn create_impl_azaleawrite(ident: &Ident, data: &Data) -> proc_macro2::Token
|
|||
} else {
|
||||
// optimization: if it doesn't have data we can just do `as u32`
|
||||
quote! {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => 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()
|
||||
),
|
||||
_ => panic!("#[derive(McBuf)] can only be used on structs"),
|
||||
}
|
||||
}
|
||||
|
|
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