From cb9230ec34abd0a0416a1baa9457072383712ee9 Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Sat, 11 Feb 2023 09:51:53 -0800 Subject: [PATCH] ECS Rewrite (#184) This PR redesigns Valence's architecture around the Bevy Entity Component System framework (`bevy_ecs` and `bevy_app`). Along the way, a large number of changes and improvements have been made. - Valence is now a Bevy plugin. This allows Valence to integrate with the wider Bevy ecosystem. - The `Config` trait has been replaced with the plugin struct which is much easier to configure. Async callbacks are grouped into their own trait. - `World` has been renamed to `Instance` to avoid confusion with `bevy_ecs::world::World`. - Entities, clients, player list, and inventories are all just ECS components/resources. There is no need for us to have our own generational arena/slotmap/etc for each one. - Client events use Bevy's event system. Users can read events with the `EventReader` system parameter. This also means that events are dispatched at an earlier stage of the program where access to the full server is available. There is a special "event loop" stage which is used primarily to avoid the loss of ordering information between events. - Chunks have been completely overhauled to be simpler and faster. The distinction between loaded and unloaded chunks has been mostly eliminated. The per-section bitset that tracked changes has been removed, which should further reduce memory usage. More operations on chunks are available such as removal and cloning. - The full client's game profile is accessible rather than just the textures. - Replaced `vek` with `glam` for parity with Bevy. - Basic inventory support has been added. - Various small changes to `valence_protocol`. - New Examples - The terrain and anvil examples are now fully asynchronous and will not block the main tick loop while chunks are loading. # TODOs - [x] Implement and dispatch client events. - ~~[ ] Finish implementing the new entity/chunk update algorithm.~~ New approach ended up being slower. And also broken. - [x] [Update rust-mc-bot to 1.19.3](https://github.com/Eoghanmc22/rust-mc-bot/pull/3). - [x] Use rust-mc-bot to test for and fix any performance regressions. Revert to old entity/chunk update algorithm if the new one turns out to be slower for some reason. - [x] Make inventories an ECS component. - [x] Make player lists an ECS ~~component~~ resource. - [x] Expose all properties of the client's game profile. - [x] Update the examples. - [x] Update `valence_anvil`. - ~~[ ] Update `valence_spatial_index` to use `glam` instead of `vek`.~~ Maybe later - [x] Make entity events use a bitset. - [x] Update docs. Closes #69 Closes #179 Closes #53 --------- Co-authored-by: Carson McManus Co-authored-by: AviiNL Co-authored-by: Danik Vitek Co-authored-by: Snowiiii <71594357+Snowiiii@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.yml | 55 + .github/ISSUE_TEMPLATE/config.yml | 4 + .github/ISSUE_TEMPLATE/feature_request.yml | 24 + .github/pull_request_template.md | 29 + .github/workflows/ci.yml | 1 + .gitignore | 4 +- CONTRIBUTING.md | 28 + Cargo.toml | 2 +- README.md | 101 +- crates/bench_players/README.md | 32 - crates/bench_players/src/main.rs | 177 -- crates/packet_inspector/src/main.rs | 4 +- .../{bench_players => playground}/Cargo.toml | 5 +- crates/playground/build.rs | 21 + crates/playground/src/.gitignore | 1 + crates/playground/src/extras.rs | 2 + crates/playground/src/main.rs | 12 + crates/playground/src/playground.template.rs | 51 + crates/valence/Cargo.toml | 28 +- crates/valence/build/entity.rs | 6 +- crates/valence/build/entity_event.rs | 59 +- crates/valence/examples/bench_players.rs | 86 + crates/valence/examples/biomes.rs | 269 +- crates/valence/examples/building.rs | 317 +-- crates/valence/examples/chest.rs | 324 +-- crates/valence/examples/combat.rs | 346 +-- crates/valence/examples/conway.rs | 427 ++-- crates/valence/examples/cow_sphere.rs | 270 +-- crates/valence/examples/death.rs | 393 +-- crates/valence/examples/entity_raycast.rs | 433 ---- crates/valence/examples/gamemode_switcher.rs | 82 + crates/valence/examples/inventory_piano.rs | 236 -- crates/valence/examples/parkour.rs | 478 ++-- crates/valence/examples/particles.rs | 266 +- crates/valence/examples/player_list.rs | 120 + crates/valence/examples/resource_pack.rs | 266 +- crates/valence/examples/server_list_ping.rs | 37 + crates/valence/examples/terrain.rs | 530 ++-- crates/valence/examples/text.rs | 310 +-- crates/valence/src/biome.rs | 16 +- crates/valence/src/chunk.rs | 1101 --------- crates/valence/src/chunk/entity_partition.rs | 179 -- crates/valence/src/chunk/pos.rs | 117 - crates/valence/src/client.rs | 2154 +++++++---------- crates/valence/src/client/event.rs | 2035 +++++++++++----- crates/valence/src/config.rs | 625 ++--- crates/valence/src/dimension.rs | 23 +- crates/valence/src/entity.rs | 735 ++---- crates/valence/src/entity/data.rs | 5 +- crates/valence/src/instance.rs | 550 +++++ crates/valence/src/instance/chunk.rs | 568 +++++ crates/valence/src/instance/chunk_entry.rs | 120 + .../{chunk => instance}/paletted_container.rs | 46 +- crates/valence/src/inventory.rs | 1119 ++++++++- crates/valence/src/lib.rs | 182 +- crates/valence/src/math.rs | 88 + crates/valence/src/packet.rs | 46 +- crates/valence/src/player_list.rs | 968 ++++---- crates/valence/src/player_textures.rs | 73 +- crates/valence/src/server.rs | 790 ++---- crates/valence/src/server/byte_channel.rs | 10 + crates/valence/src/server/connect.rs | 503 ++++ .../{packet_manager.rs => connection.rs} | 165 +- crates/valence/src/server/login.rs | 302 --- crates/valence/src/slab.rs | 340 --- crates/valence/src/slab_rc.rs | 128 - crates/valence/src/slab_versioned.rs | 198 -- crates/valence/src/unit_test/example.rs | 136 ++ crates/valence/src/unit_test/mod.rs | 2 + crates/valence/src/unit_test/util.rs | 204 ++ crates/valence/src/util.rs | 81 - crates/valence/src/view.rs | 193 ++ crates/valence/src/world.rs | 200 -- crates/valence_anvil/Cargo.toml | 12 +- crates/valence_anvil/benches/world_parsing.rs | 4 +- .../valence_anvil/examples/anvil_loading.rs | 208 ++ .../valence_anvil/examples/valence_loading.rs | 202 -- crates/valence_anvil/src/to_valence.rs | 13 +- crates/valence_protocol/Cargo.toml | 2 +- crates/valence_protocol/benches/benches.rs | 7 +- crates/valence_protocol/src/codec.rs | 89 +- crates/valence_protocol/src/impls.rs | 13 +- crates/valence_protocol/src/inventory.rs | 62 - crates/valence_protocol/src/lib.rs | 40 +- crates/valence_protocol/src/packets/s2c.rs | 91 +- .../src/packets/s2c/map_data.rs | 9 +- .../src/packets/s2c/player_chat_message.rs | 13 +- .../src/packets/s2c/player_info_update.rs | 16 +- .../src/packets/s2c/update_advancements.rs | 9 +- .../src/packets/s2c/update_teams.rs | 13 +- crates/valence_protocol/src/text.rs | 12 + crates/valence_protocol/src/types.rs | 43 +- crates/valence_protocol/src/username.rs | 11 +- .../Cargo.toml | 2 +- .../src/decode.rs | 0 .../src/encode.rs | 0 .../src/lib.rs | 0 extractor/build.gradle | 2 +- extractor/gradle.properties | 19 +- 99 files changed, 9842 insertions(+), 10888 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/pull_request_template.md delete mode 100644 crates/bench_players/README.md delete mode 100644 crates/bench_players/src/main.rs rename crates/{bench_players => playground}/Cargo.toml (63%) create mode 100644 crates/playground/build.rs create mode 100644 crates/playground/src/.gitignore create mode 100644 crates/playground/src/extras.rs create mode 100644 crates/playground/src/main.rs create mode 100644 crates/playground/src/playground.template.rs create mode 100644 crates/valence/examples/bench_players.rs delete mode 100644 crates/valence/examples/entity_raycast.rs create mode 100644 crates/valence/examples/gamemode_switcher.rs delete mode 100644 crates/valence/examples/inventory_piano.rs create mode 100644 crates/valence/examples/player_list.rs create mode 100644 crates/valence/examples/server_list_ping.rs delete mode 100644 crates/valence/src/chunk.rs delete mode 100644 crates/valence/src/chunk/entity_partition.rs delete mode 100644 crates/valence/src/chunk/pos.rs create mode 100644 crates/valence/src/instance.rs create mode 100644 crates/valence/src/instance/chunk.rs create mode 100644 crates/valence/src/instance/chunk_entry.rs rename crates/valence/src/{chunk => instance}/paletted_container.rs (91%) create mode 100644 crates/valence/src/math.rs create mode 100644 crates/valence/src/server/connect.rs rename crates/valence/src/server/{packet_manager.rs => connection.rs} (61%) delete mode 100644 crates/valence/src/server/login.rs delete mode 100644 crates/valence/src/slab.rs delete mode 100644 crates/valence/src/slab_rc.rs delete mode 100644 crates/valence/src/slab_versioned.rs create mode 100644 crates/valence/src/unit_test/example.rs create mode 100644 crates/valence/src/unit_test/mod.rs create mode 100644 crates/valence/src/unit_test/util.rs delete mode 100644 crates/valence/src/util.rs create mode 100644 crates/valence/src/view.rs delete mode 100644 crates/valence/src/world.rs create mode 100644 crates/valence_anvil/examples/anvil_loading.rs delete mode 100644 crates/valence_anvil/examples/valence_loading.rs delete mode 100644 crates/valence_protocol/src/inventory.rs rename crates/{valence_derive => valence_protocol_macros}/Cargo.toml (84%) rename crates/{valence_derive => valence_protocol_macros}/src/decode.rs (100%) rename crates/{valence_derive => valence_protocol_macros}/src/encode.rs (100%) rename crates/{valence_derive => valence_protocol_macros}/src/lib.rs (100%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..c7771c7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,55 @@ +name: Report a Bug +description: Is there something not working like you expected? Let us know! +labels: [bug, triage] +body: + - type: input + attributes: + label: Valence Version + description: What version of Valence are you using? If you are targeting a specific commit or branch, put that instead. + placeholder: version, branch, or commit hash + validations: + required: true + - type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: false + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Minimal Reproducible Example + description: Do you have a playground to reproduce this bug? Paste it here. It should be a minimal example that exhibits the behavior. If the problem can be reproduced with one of the examples, just indicate which one instead. + render: Rust + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Create a server using my sample + 2. Do this... + 3. Do that... + 4. See error + validations: + required: true + - type: checkboxes + attributes: + label: Checklist + options: + - label: I have searched the issues of this repo and believe that this is not a duplicate. + required: true + - label: This used to work before. + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Version Numbers? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6a45a5b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: Question/Troubleshooting + url: https://discord.gg/8Fqqy9XrYb + about: If you just have a question or need help, feel free to reach out on our Discord server. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..31acf47 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,24 @@ +name: Feature Request +description: Do you have an idea for a new feature? Let us know! +labels: [enhancement] +body: + - type: textarea + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. + placeholder: I'm always frustrated when... + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + placeholder: I would like to be able to... + validations: + required: true + - type: textarea + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..4add78b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ + + + +## Description + + + +## Test Plan + + + + +
+ +Playground + +```rust +PASTE YOUR PLAYGROUND CODE HERE +``` + +
+ + +Steps: +1. + +#### Related + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 408968f..969a694 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: with: components: clippy, rustfmt + - run: cp crates/playground/src/playground.template.rs crates/playground/src/playground.rs - name: Validate formatting run: cargo fmt --all -- --check - name: Validate documentation diff --git a/.gitignore b/.gitignore index 95163d9..b808c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,9 @@ Cargo.lock /extractor/out /extractor/classes /extractor/run -/crates/*/rust-mc-bot +rust-mc-bot .asset_cache/ /velocity -flamegraph.svg +flamegraph*.svg perf.data perf.data.old diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c6395c..b018025 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,27 @@ knowledge to complete. New contributors are not required to start with these iss If you plan to work on something that's not an open issue, consider making one first so that it can be discussed. This way, your contribution will not be rejected when it is submitted for review. +## Playgrounds and Examples + +Examples (found in the `examples/` directory) are a great way to document how pieces of Valence's API fit together. It's important that they remain as simple as possible. If you're working on a feature that requires a more complex scenario, consider making a playground instead. + +Examples can be run with `cargo run -p valence --example `. + +Playgrounds are meant to provide a quick and minimal environment to test out new code or reproduce bugs. Playgrounds are also a great way test out quick ideas. This is the preferred method for providing code samples in issues and pull requests. + +To get started with a new playground, copy the template to `playground.rs`. + +```bash +cp crates/playground/src/playground.template.rs crates/playground/src/playground.rs +``` + +Make your changes to `crates/playground/src/playground.rs`. To run it: + +```bash +cargo run -p playground # simply run the playground, or +cargo watch -c -x "run -p playground" # run the playground and watch for changes +``` + # Automatic Checks When you submit a pull request, your code will automatically run through clippy, rustfmt, etc. to check for any errors. @@ -104,3 +125,10 @@ to name this variable `num_foos`. All public items should be documented. Documentation must be written with complete sentences and correct grammar. Consider using [intra-doc links](https://doc.rust-lang.org/rustdoc/write-documentation/linking-to-items-by-name.html) where appropriate. + +## Unit Tests + +Unit tests help your contributions last! They ensure that your code works as expected and that it continues to work in +the future. + +You can find examples of unit tests in the `unit_test/example.rs` module. diff --git a/Cargo.toml b/Cargo.toml index 006314c..b25f675 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = ["crates/*"] -exclude = ["crates/*/rust-mc-bot"] +exclude = ["rust-mc-bot"] [profile.dev.package."*"] opt-level = 3 diff --git a/README.md b/README.md index 4d8f202..3c9f128 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,88 @@ - +

+ +

-_**NOTE:** Valence is currently undergoing a major rewrite. The information in this README may be outdated. See [ECS Rewrite](https://github.com/valence-rs/valence/pull/184) for more information._ - ---- +

+ + license + + + + chat on Discord + + GitHub sponsors +

A Rust framework for building Minecraft: Java Edition servers. -Like [feather](https://github.com/feather-rs/feather), Valence is an effort to build a Minecraft compatible server -completely from scratch in Rust. The difference is that Valence has decided to organize the effort a little differently. -All game logic is behind a trait. This approach has many advantages. Features such as a plugin system, dedicated -executable, and vanilla game mechanics can be implemented _on top of_ Valence. Valence is a Rust library like any other. +Built on top of [Bevy ECS](https://bevyengine.org/learn/book/getting-started/ecs/), Valence is an effort to create a +Minecraft compatible server completely from scratch in Rust. You can think of Valence as a _game engine for +Minecraft servers_. It doesn't do much by default, but by writing game logic yourself and leveraging Bevy's +powerful [plugin system](https://bevyengine.org/learn/book/getting-started/plugins/), you can make almost anything. -In the future we may decide to reimplement vanilla game mechanics as a separate project. If you're developing something -like a minigame server without need for vanilla game mechanics, you can depend on Valence directly. +Opinionated features like dynamic scripting, dedicated executables, and vanilla game mechanics are all expected to be +built as optional plugins. This level of modularity is desirable for those looking to build highly custom experiences +in Minecraft such as minigame servers. # Goals Valence aims to be the following: * **Complete**. Abstractions for the full breadth of the Minecraft protocol. -* **Flexible**. Valence provides direct access to Minecraft's protocol when necessary. -* **Minimal**. The API surface is small with only the necessities exposed. Opinionated features such as a - standalone executable, plugin system, and reimplementation of vanilla mechanics should be built in a separate project on - top of the foundation that Valence provides. -* **Intuitive**. An API that is easy to use and difficult to misuse. Extensive documentation is important. -* **Efficient**. Optimal use of system resources with multiple CPU cores in mind. +* **Flexible**. Valence provides direct access to the lowest levels of Minecraft's protocol when necessary. +* **Modular**. Pick and choose the features you actually need. +* **Intuitive**. An API that is easy to use and difficult to misuse. Extensive documentation and examples are important. +* **Efficient**. Optimal use of system resources with multiple CPU cores in mind. Valence uses very little memory and + can + support [thousands](https://cdn.discordapp.com/attachments/998132822864834613/1051100042519380028/2022-12-10_03.30.09.png) + of players at the same time without lag (assuming you have the bandwidth). * **Up to date**. Targets the most recent stable version of Minecraft. Support for multiple versions at once is not - planned (although you can use a proxy). + planned. However, you can use a proxy with [ViaBackwards](https://www.spigotmc.org/resources/viabackwards.27448/) to + achieve backwards compatibility with older clients. ## Current Status Valence is still early in development with many features unimplemented or incomplete. However, the foundations are in place. Here are some noteworthy achievements: -- [x] A new serde library for Minecraft's Named Binary Tag (NBT) format -- [x] Authentication, encryption, and compression -- [x] Block states -- [x] Chunks -- [x] Entities and tracked data -- [x] Bounding volume hierarchy for fast spatial entity queries -- [x] Player list and player skins -- [x] Dimensions, biomes, and worlds -- [x] JSON Text API -- [x] A Fabric mod for extracting data from the game into JSON files. These files are processed by a build script to +- `valence_nbt`: A speedy new library for Minecraft's Named Binary Tag (NBT) format. +- `valence_protocol`: A library for working with Minecraft's protocol. Does not depend on Valence and can be used in + other projects. +- Authentication, encryption, and compression +- Block states +- Chunks +- Entities and metadata +- Bounding volume hierarchy for fast spatial entity queries +- Player list and player skins +- Dimensions, biomes, and worlds +- JSON Text API +- A Fabric mod for extracting data from the game into JSON files. These files are processed by a build script to generate Rust code for the project. The JSON files can be used in other projects as well. -- [x] Items -- [x] Particles -- [x] Anvil file format (read only) -- [ ] Inventory -- [ ] Block entities -- [x] Proxy support ([Velocity](https://velocitypowered.com/), [Bungeecord](https://www.spigotmc.org/wiki/bungeecord/) and [Waterfall](https://docs.papermc.io/waterfall)) -- [ ] Utilities for continuous collision detection +- Inventories +- Items +- Particles +- Anvil file format (read only) +- Proxy support ([Velocity](https://velocitypowered.com/), [Bungeecord](https://www.spigotmc.org/wiki/bungeecord/) + and [Waterfall](https://docs.papermc.io/waterfall)) -Here is a [short video](https://www.youtube.com/watch?v=6P072lKE01s) showing the examples and some of its current -capabilities. +Here is a [short video](https://www.youtube.com/watch?v=6P072lKE01s) (outdated) showing the examples and some of +Valence's capabilities. # Getting Started ## Running the Examples -You may want to try running one of the examples. After cloning the repository, run +After cloning the repository, run ```shell -cargo r -r --example conway +cargo r -r --example ``` +to view the list of examples. I recommend giving `parkour`, `conway`, `terrain`, and `cow_sphere` a try. + Next, open your Minecraft client and connect to the address `localhost`. If all goes well you should be playing on the server. @@ -75,14 +93,13 @@ project. Documentation is available [here](https://docs.rs/valence/latest/valenc However, the crates.io version is likely outdated. To use the most recent development version, add Valence as a [git dependency](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories) -. ```toml [dependencies] valence = { git = "https://github.com/valence-rs/valence" } ``` -View the documentation by running `cargo d --open` in your project. +View the latest documentation by running `cargo d --open` in your project. # Contributing @@ -97,8 +114,8 @@ under [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/) # Funding -If you would like to contribute financially consider sponsoring me (rj00a) +If you would like to contribute financially, consider sponsoring me (rj00a) on [GitHub](https://github.com/sponsors/rj00a) -or [Patreon](https://www.patreon.com/rj00a) (GitHub is preferred). +or [Patreon](https://www.patreon.com/rj00a). I would love to continue working on Valence and your support would help me do that. Thanks! diff --git a/crates/bench_players/README.md b/crates/bench_players/README.md deleted file mode 100644 index 771eeac..0000000 --- a/crates/bench_players/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Performance Tests - -Run the server - -```shell -cargo r -r -p bench_players -``` - -In a separate terminal, start [rust-mc-bot](https://github.com/Eoghanmc22/rust-mc-bot). -This command should connect 1000 clients to the server. - -```shell -# In the rust-mc-bot directory -cargo r -r -- 127.0.0.1:25565 1000 -``` - -If the delta time is consistently >50ms, the server is running behind schedule. - -Note: - -# Flamegraph - -To start capturing a [flamegraph](https://github.com/flamegraph-rs/flamegraph), -run the server like this: - -```shell -# You can also try setting the `CARGO_PROFILE_RELEASE_DEBUG` environment variable to `true`. -cargo flamegraph -p bench_players -``` - -Run rust-mc-bot as above, and then stop the server after a few seconds. Flamegraph will generate a flamegraph.svg in the -current directory. You can then open that file in your internet browser of choice. diff --git a/crates/bench_players/src/main.rs b/crates/bench_players/src/main.rs deleted file mode 100644 index 12c7841..0000000 --- a/crates/bench_players/src/main.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::net::SocketAddr; - -use valence::prelude::*; - -pub fn main() -> ShutdownResult { - tracing_subscriber::fmt().init(); - - valence::start_server( - Game, - ServerState { - player_list: None, - millis_sum: 0.0, - }, - ) -} - -const WITH_PLAYER_ENTITIES: bool = true; - -struct Game; - -struct ServerState { - player_list: Option, - millis_sum: f64, -} - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = EntityId; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - fn max_connections(&self) -> usize { - 10_000 - } - - fn connection_mode(&self) -> ConnectionMode { - ConnectionMode::Offline - } - - fn outgoing_capacity(&self) -> usize { - usize::MAX - } - - fn dimensions(&self) -> Vec { - vec![Dimension { - natural: false, - ambient_light: 1.0, - fixed_time: None, - effects: Default::default(), - min_y: 0, - height: 256, - }] - } - - async fn server_list_ping( - &self, - _shared: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: -1, - max_players: -1, - player_sample: Default::default(), - description: "Player Benchmark Server".into(), - favicon_png: None, - } - } - - fn init(&self, server: &mut Server) { - server.state.player_list = Some(server.player_lists.insert(()).0); - - let (_, world) = server.worlds.insert(DimensionId::default(), ()); - - let size = 5; - for chunk_z in -size..size { - for chunk_x in -size..size { - let mut chunk = UnloadedChunk::new(16); - for z in 0..16 { - for x in 0..16 { - chunk.set_block_state(x, 0, z, BlockState::GRASS_BLOCK); - } - } - - world.chunks.insert([chunk_x, chunk_z], chunk, ()); - } - } - } - - fn update(&self, server: &mut Server) { - { - let millis = server.last_tick_duration().as_secs_f64() * 1000.0; - let tick = server.current_tick(); - let players = server.clients.len(); - let delay = 20; - - server.millis_sum += millis; - - if tick % delay == 0 { - let avg = server.millis_sum / delay as f64; - println!("Avg MSPT: {avg:.3}ms, Tick={tick}, Players={players}"); - server.millis_sum = 0.0; - } - } - - let (world_id, _) = server.worlds.iter_mut().next().unwrap(); - - server.clients.retain(|_, client| { - if client.created_this_tick() { - client.respawn(world_id); - client.set_flat(true); - client.teleport([0.0, 1.0, 0.0], 0.0, 0.0); - client.set_game_mode(GameMode::Creative); - - if WITH_PLAYER_ENTITIES { - client.set_player_list(server.state.player_list.clone()); - if let Some(id) = &server.state.player_list { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, entity)) => { - entity.set_world(world_id); - client.state = id - } - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } - } - } - - if client.is_disconnected() { - if WITH_PLAYER_ENTITIES { - if let Some(id) = &server.state.player_list { - server.player_lists[id].remove(client.uuid()); - } - server.entities[client.state].set_deleted(true); - } - - return false; - } - - if WITH_PLAYER_ENTITIES { - if let Some(player) = server.entities.get_mut(client.state) { - while let Some(event) = client.next_event() { - event.handle_default(client, player); - } - } - } else { - while let Some(event) = client.next_event() { - if let ClientEvent::UpdateSettings { view_distance, .. } = event { - client.set_view_distance(view_distance); - } - } - } - - true - }); - } -} diff --git a/crates/packet_inspector/src/main.rs b/crates/packet_inspector/src/main.rs index f82aaf7..0446a94 100644 --- a/crates/packet_inspector/src/main.rs +++ b/crates/packet_inspector/src/main.rs @@ -1,9 +1,9 @@ use std::error::Error; use std::fmt::Write; +use std::io; use std::io::ErrorKind; use std::net::SocketAddr; use std::sync::Arc; -use std::{fmt, io}; use anyhow::bail; use clap::Parser; @@ -63,7 +63,7 @@ struct State { impl State { pub async fn rw_packet<'a, P>(&'a mut self) -> anyhow::Result

where - P: DecodePacket<'a> + EncodePacket + fmt::Debug, + P: DecodePacket<'a> + EncodePacket, { while !self.dec.has_next_packet()? { self.dec.reserve(4096); diff --git a/crates/bench_players/Cargo.toml b/crates/playground/Cargo.toml similarity index 63% rename from crates/bench_players/Cargo.toml rename to crates/playground/Cargo.toml index 00141e0..e47b736 100644 --- a/crates/bench_players/Cargo.toml +++ b/crates/playground/Cargo.toml @@ -1,8 +1,11 @@ [package] -name = "bench_players" +name = "playground" version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0.65" +glam = "0.22.0" +tracing = "0.1.37" tracing-subscriber = "0.3.16" valence = { path = "../valence" } diff --git a/crates/playground/build.rs b/crates/playground/build.rs new file mode 100644 index 0000000..4f75cfb --- /dev/null +++ b/crates/playground/build.rs @@ -0,0 +1,21 @@ +use std::path::Path; + +fn main() { + let current = std::env::current_dir().unwrap(); + println!("current directory: {}", current.display()); + + let src = current.join(Path::new("src/playground.template.rs")); + let dst = current.join(Path::new("src/playground.rs")); + + if dst.exists() { + println!("{dst:?} already exists, skipping"); + return; + } + + if !src.exists() { + println!("{src:?} does not exist, skipping"); + return; + } + + std::fs::copy(src, dst).unwrap(); +} diff --git a/crates/playground/src/.gitignore b/crates/playground/src/.gitignore new file mode 100644 index 0000000..68aefd1 --- /dev/null +++ b/crates/playground/src/.gitignore @@ -0,0 +1 @@ +playground.rs \ No newline at end of file diff --git a/crates/playground/src/extras.rs b/crates/playground/src/extras.rs new file mode 100644 index 0000000..7a43fdc --- /dev/null +++ b/crates/playground/src/extras.rs @@ -0,0 +1,2 @@ +//! Put stuff in here if you find that you have to write the same code for +//! multiple playgrounds. diff --git a/crates/playground/src/main.rs b/crates/playground/src/main.rs new file mode 100644 index 0000000..47dac45 --- /dev/null +++ b/crates/playground/src/main.rs @@ -0,0 +1,12 @@ +use valence::bevy_app::App; + +mod extras; +mod playground; + +fn main() { + tracing_subscriber::fmt().init(); + + let mut app = App::new(); + playground::build_app(&mut app); + app.run(); +} diff --git a/crates/playground/src/playground.template.rs b/crates/playground/src/playground.template.rs new file mode 100644 index 0000000..7ba539c --- /dev/null +++ b/crates/playground/src/playground.template.rs @@ -0,0 +1,51 @@ +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; +use valence::prelude::*; + +#[allow(unused_imports)] +use crate::extras::*; + +const SPAWN_Y: i32 = 64; + +pub fn build_app(app: &mut App) { + app.add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients); +} + +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); + + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); + } + } + + for z in -25..25 { + for x in -25..25 { + instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + world.spawn(instance); +} + +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + let instance = instances.get_single().unwrap(); + + for mut client in &mut clients { + client.set_position([0.5, SPAWN_Y as f64 + 1.0, 0.5]); + client.set_instance(instance); + client.set_game_mode(GameMode::Survival); + } +} + +// Add new systems here! diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index 5eb9d17..766d61a 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -6,7 +6,7 @@ description = "A framework for building Minecraft servers in Rust." repository = "https://github.com/rj00a/valence" readme = "README.md" license = "MIT" -keywords = ["minecraft", "gamedev", "server"] +keywords = ["minecraft", "gamedev", "server", "ecs"] categories = ["game-engines"] build = "build/main.rs" authors = ["Ryan Johnson "] @@ -14,18 +14,20 @@ authors = ["Ryan Johnson "] [dependencies] anyhow = "1.0.65" arrayvec = "0.7.2" -async-trait = "0.1.57" -base64 = "0.13.0" -bitfield-struct = "0.1.8" +async-trait = "0.1.60" +base64 = "0.21.0" +bevy_app = "0.9.1" +bevy_ecs = "0.9.1" +bitfield-struct = "0.3.1" bytes = "1.2.1" flume = "0.10.14" -futures = "0.3.24" +glam = "0.22.0" hmac = "0.12.1" num = "0.4.0" -paste = "1.0.9" +parking_lot = "0.12.1" +paste = "1.0.11" rand = "0.8.5" -rayon = "1.5.3" -rsa = "0.6.1" +rsa = "0.7.2" rsa-der = "0.3.0" rustc-hash = "1.1.0" serde = { version = "1.0.145", features = ["derive"] } @@ -33,16 +35,12 @@ serde_json = "1.0.85" sha1 = "0.10.5" sha2 = "0.10.6" thiserror = "1.0.35" +tokio = { version = "1.25.0", features = ["full"] } tracing = "0.1.37" url = { version = "2.2.2", features = ["serde"] } uuid = { version = "1.1.2", features = ["serde"] } valence_nbt = { version = "0.5.0", path = "../valence_nbt" } valence_protocol = { version = "0.1.0", path = "../valence_protocol", features = ["encryption", "compression"] } -vek = "0.15.8" - -[dependencies.tokio] -version = "1.21.2" -features = ["macros", "rt-multi-thread", "net", "io-util", "sync", "time"] [dependencies.reqwest] version = "0.11.12" @@ -52,9 +50,9 @@ features = ["rustls-tls", "json"] [dev-dependencies] approx = "0.5.1" -tracing-subscriber = "0.3.16" +glam = { version = "0.22.0", features = ["approx"] } noise = "0.8.2" -valence_spatial_index = { path = "../valence_spatial_index", version = "0.1.0" } +tracing-subscriber = "0.3.16" [build-dependencies] anyhow = "1.0.65" diff --git a/crates/valence/build/entity.rs b/crates/valence/build/entity.rs index 7f8bd77..3972f97 100644 --- a/crates/valence/build/entity.rs +++ b/crates/valence/build/entity.rs @@ -128,7 +128,7 @@ impl Value { Value::Facing(_) => quote!(Facing), Value::OptionalUuid(_) => quote!(Option), Value::OptionalBlockState(_) => quote!(BlockState), - Value::NbtCompound(_) => quote!(crate::nbt::Compound), + Value::NbtCompound(_) => quote!(valence_nbt::Compound), Value::Particle(_) => quote!(Particle), Value::VillagerData { .. } => quote!(VillagerData), Value::OptionalInt(_) => quote!(OptionalInt), @@ -145,7 +145,7 @@ impl Value { Value::String(_) => quote!(&str), Value::TextComponent(_) => quote!(&Text), Value::OptionalTextComponent(_) => quote!(Option<&Text>), - Value::NbtCompound(_) => quote!(&crate::nbt::Compound), + Value::NbtCompound(_) => quote!(&valence_nbt::Compound), _ => self.field_type(), } } @@ -191,7 +191,7 @@ impl Value { } Value::OptionalUuid(_) => quote!(None), // TODO Value::OptionalBlockState(_) => quote!(BlockState::default()), // TODO - Value::NbtCompound(_) => quote!(crate::nbt::Compound::default()), // TODO + Value::NbtCompound(_) => quote!(valence_nbt::Compound::default()), // TODO Value::Particle(p) => { let variant = ident(p.to_pascal_case()); quote!(Particle::#variant) diff --git a/crates/valence/build/entity_event.rs b/crates/valence/build/entity_event.rs index eeae2a6..01eb982 100644 --- a/crates/valence/build/entity_event.rs +++ b/crates/valence/build/entity_event.rs @@ -8,13 +8,13 @@ use serde::Deserialize; use crate::ident; #[derive(Deserialize, Clone, Debug)] -struct EntityData { +struct EntityEvents { statuses: BTreeMap, animations: BTreeMap, } pub fn build() -> anyhow::Result { - let entity_data: EntityData = + let entity_data: EntityEvents = serde_json::from_str(include_str!("../../../extracted/entity_data.json"))?; let mut statuses: Vec<_> = entity_data.statuses.into_iter().collect(); @@ -23,44 +23,39 @@ pub fn build() -> anyhow::Result { let mut animations: Vec<_> = entity_data.animations.into_iter().collect(); animations.sort_by_key(|(_, id)| *id); - let event_variants = statuses + let entity_status_variants: Vec<_> = statuses .iter() - .chain(animations.iter()) - .map(|(name, _)| ident(name.to_pascal_case())); + .map(|(name, code)| { + let name = ident(name.to_pascal_case()); + let code = *code as isize; - let status_arms = statuses.iter().map(|(name, code)| { - let name = ident(name.to_pascal_case()); - quote! { - Self::#name => StatusOrAnimation::Status(#code), - } - }); + quote! { + #name = #code, + } + }) + .collect(); - let animation_arms = animations.iter().map(|(name, code)| { - let name = ident(name.to_pascal_case()); - quote! { - Self::#name => StatusOrAnimation::Animation(#code), - } - }); + let entity_animation_variants: Vec<_> = animations + .iter() + .map(|(name, code)| { + let name = ident(name.to_pascal_case()); + let code = *code as isize; + + quote! { + #name = #code, + } + }) + .collect(); Ok(quote! { #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] - pub enum EntityEvent { - #(#event_variants,)* + pub enum EntityStatus { + #(#entity_status_variants)* } - impl EntityEvent { - pub(crate) fn status_or_animation(self) -> StatusOrAnimation { - match self { - #(#status_arms)* - #(#animation_arms)* - } - } - } - - #[derive(Clone, Copy, PartialEq, Eq, Debug)] - pub(crate) enum StatusOrAnimation { - Status(u8), - Animation(u8), + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] + pub enum EntityAnimation { + #(#entity_animation_variants)* } }) } diff --git a/crates/valence/examples/bench_players.rs b/crates/valence/examples/bench_players.rs new file mode 100644 index 0000000..84d7fd3 --- /dev/null +++ b/crates/valence/examples/bench_players.rs @@ -0,0 +1,86 @@ +use std::time::Instant; + +use bevy_app::{App, CoreStage}; +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; +use valence::instance::{Chunk, Instance}; +use valence::prelude::*; + +const SPAWN_Y: i32 = 64; + +#[derive(Resource)] +struct TickStart(Instant); + +fn main() { + tracing_subscriber::fmt().init(); + + App::new() + .add_plugin( + ServerPlugin::new(()) + .with_connection_mode(ConnectionMode::Offline) + .with_compression_threshold(None) + .with_max_connections(50_000), + ) + .add_startup_system(setup) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_to_stage(CoreStage::First, record_tick_start_time) + .add_system_to_stage(CoreStage::Last, print_tick_time) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .add_system_set(PlayerList::default_system_set()) + .run(); +} + +fn record_tick_start_time(world: &mut World) { + world + .get_resource_or_insert_with(|| TickStart(Instant::now())) + .0 = Instant::now(); +} + +fn print_tick_time(server: Res, time: Res, clients: Query<(), With>) { + let tick = server.current_tick(); + if tick % (server.tps() / 2) == 0 { + let client_count = clients.iter().count(); + + let millis = time.0.elapsed().as_secs_f32() * 1000.0; + println!("Tick={tick}, MSPT={millis:.04}ms, Clients={client_count}"); + } +} + +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); + + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); + } + } + + for z in -50..50 { + for x in -50..50 { + instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + world.spawn(instance); +} + +fn init_clients( + mut clients: Query<(Entity, &mut Client), Added>, + instances: Query>, + mut commands: Commands, +) { + let instance = instances.get_single().unwrap(); + + for (client_entity, mut client) in &mut clients { + client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + client.set_instance(instance); + client.set_game_mode(GameMode::Creative); + + let player_entity = McEntity::with_uuid(EntityKind::Player, instance, client.uuid()); + + commands.entity(client_entity).insert(player_entity); + } +} diff --git a/crates/valence/examples/biomes.rs b/crates/valence/examples/biomes.rs index a8172c8..4a2ad72 100644 --- a/crates/valence/examples/biomes.rs +++ b/crates/valence/examples/biomes.rs @@ -1,208 +1,89 @@ -use std::iter; -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; - +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; use valence::prelude::*; -pub fn main() -> ShutdownResult { +const SPAWN_Y: i32 = 0; +const BIOME_COUNT: usize = 10; + +pub fn main() { tracing_subscriber::fmt().init(); - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - ServerState { player_list: None }, - ) -} - -struct Game { - player_count: AtomicUsize, -} - -struct ServerState { - player_list: Option, -} - -#[derive(Default)] -struct ClientState { - entity_id: EntityId, -} - -const MAX_PLAYERS: usize = 10; -const BIOME_COUNT: usize = 10; -const MIN_Y: i32 = -64; - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - fn dimensions(&self) -> Vec { - vec![Dimension { - fixed_time: Some(6000), - ..Dimension::default() - }] - } - - fn biomes(&self) -> Vec { - (1..BIOME_COUNT) - .map(|i| { - let color = (0xffffff / BIOME_COUNT * i) as u32; - Biome { - name: ident!("valence:test_biome_{i}"), - sky_color: color, - water_fog_color: color, - fog_color: color, - water_color: color, - foliage_color: Some(color), - grass_color: Some(color), - ..Default::default() - } - }) - .chain(iter::once(Biome { - name: ident!("plains"), - ..Default::default() - })) - .collect() - } - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), - } - } - - fn init(&self, server: &mut Server) { - let world = server.worlds.insert(DimensionId::default(), ()).1; - server.state.player_list = Some(server.player_lists.insert(()).0); - - let height = world.chunks.height(); - assert_eq!(world.chunks.min_y(), MIN_Y); - - for chunk_z in 0..3 { - for chunk_x in 0..3 { - let chunk = if chunk_x == 1 && chunk_z == 1 { - let mut chunk = UnloadedChunk::new(height); - - // Set chunk blocks - for z in 0..16 { - for x in 0..16 { - chunk.set_block_state(x, 1, z, BlockState::GRASS_BLOCK); + App::new() + .add_plugin( + ServerPlugin::new(()).with_biomes( + (1..BIOME_COUNT) + .map(|i| { + let color = (0xffffff / BIOME_COUNT * i) as u32; + Biome { + name: ident!("valence:test_biome_{i}"), + sky_color: color, + water_fog_color: color, + fog_color: color, + water_color: color, + foliage_color: Some(color), + grass_color: Some(color), + ..Default::default() } - } - - // Set chunk biomes - for z in 0..4 { - for x in 0..4 { - for y in 0..height / 4 { - let biome_id = server - .shared - .biomes() - .nth((x + z * 4 + y * 4 * 4) % BIOME_COUNT) - .unwrap() - .0; - - chunk.set_biome(x, y, z, biome_id); - } - } - } - - chunk - } else { - UnloadedChunk::default() - }; - - world.chunks.insert([chunk_x, chunk_z], chunk, ()); - } - } - } - - fn update(&self, server: &mut Server) { - let (world_id, _) = server.worlds.iter_mut().next().unwrap(); - - let spawn_pos = [24.0, 50.0, 24.0]; - - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } + .chain(std::iter::once(Biome { + name: ident!("plains"), + ..Default::default() + })) + .collect::>(), + ), + ) + .add_system_to_stage(EventLoop, default_event_handler) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .add_system_set(PlayerList::default_system_set()) + .run(); +} - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, entity)) => { - entity.set_world(world_id); - client.entity_id = id - } - None => { - client.disconnect("Conflicting UUID"); - return false; +fn setup(world: &mut World) { + let server = world.resource::(); + let mut instance = server.new_instance(DimensionId::default()); + + for z in -5..5 { + for x in -5..5 { + let mut chunk = Chunk::new(4); + // Set chunk blocks + for z in 0..16 { + for x in 0..16 { + chunk.set_block_state(x, 63, z, BlockState::GRASS_BLOCK); + } + } + + // Set the biomes of the chunk to a 4x4x4 grid of biomes + for cz in 0..4 { + for cx in 0..4 { + let height = chunk.section_count() * 16; + for cy in 0..height / 4 { + let biome_id = server + .biomes() + .nth((cx + cz * 4 + cy * 4 * 4) % BIOME_COUNT) + .unwrap() + .0; + chunk.set_biome(cx, cy, cz, biome_id); } } - - client.respawn(world_id); - client.set_flat(true); - client.teleport(spawn_pos, 0.0, 0.0); - client.set_player_list(server.state.player_list.clone()); - - if let Some(id) = &server.state.player_list { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - - client.set_game_mode(GameMode::Creative); } + instance.insert_chunk([x, z], chunk); + } + } - while client.next_event().is_some() {} + world.spawn(instance); +} - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities[client.entity_id].set_deleted(true); - if let Some(id) = &server.state.player_list { - server.player_lists[id].remove(client.uuid()); - } - return false; - } - - if client.position().y < MIN_Y as _ { - client.teleport(spawn_pos, client.yaw(), client.pitch()); - } - - true - }); +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + for mut client in &mut clients { + client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + client.set_respawn_screen(true); + client.set_instance(instances.single()); + client.set_game_mode(GameMode::Creative); + client.send_message("Welcome to Valence!".italic()); } } diff --git a/crates/valence/examples/building.rs b/crates/valence/examples/building.rs index 3c1098f..795402f 100644 --- a/crates/valence/examples/building.rs +++ b/crates/valence/examples/building.rs @@ -1,201 +1,152 @@ -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; - +use valence::client::despawn_disconnected_clients; +use valence::client::event::{ + default_event_handler, FinishDigging, StartDigging, StartSneaking, UseItemOnBlock, +}; use valence::prelude::*; +use valence_protocol::types::Hand; -pub fn main() -> ShutdownResult { +const SPAWN_Y: i32 = 64; + +pub fn main() { tracing_subscriber::fmt().init(); - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - ServerState { player_list: None }, - ) + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_to_stage(EventLoop, toggle_gamemode_on_sneak) + .add_system_to_stage(EventLoop, digging_creative_mode) + .add_system_to_stage(EventLoop, digging_survival_mode) + .add_system_to_stage(EventLoop, place_blocks) + .add_system_set(PlayerList::default_system_set()) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .run(); } -struct Game { - player_count: AtomicUsize, -} +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); -struct ServerState { - player_list: Option, -} - -#[derive(Default)] -struct ClientState { - entity_id: EntityId, -} - -const MAX_PLAYERS: usize = 10; - -const SIZE_X: i32 = 100; -const SIZE_Z: i32 = 100; - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - fn dimensions(&self) -> Vec { - vec![Dimension { - fixed_time: Some(6000), - ..Dimension::default() - }] - } - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); } } - fn init(&self, server: &mut Server) { - let world = server.worlds.insert(DimensionId::default(), ()).1; - server.state.player_list = Some(server.player_lists.insert(()).0); - - // initialize chunks - for z in 0..SIZE_Z { - for x in 0..SIZE_X { - world - .chunks - .set_block_state([x, 0, z], BlockState::GRASS_BLOCK); - } + for z in -25..25 { + for x in -25..25 { + instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } - fn update(&self, server: &mut Server) { - let (world_id, world) = server.worlds.iter_mut().next().unwrap(); + world.spawn(instance); +} - let spawn_pos = [SIZE_X as f64 / 2.0, 1.0, SIZE_Z as f64 / 2.0]; +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + for mut client in &mut clients { + client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + client.set_instance(instances.single()); + client.set_game_mode(GameMode::Creative); + client.send_message("Welcome to Valence! Build something cool.".italic()); + } +} - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } - - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, entity)) => { - entity.set_world(world_id); - client.entity_id = id - } - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } - - client.respawn(world_id); - client.set_flat(true); - client.teleport(spawn_pos, 0.0, 0.0); - client.set_player_list(server.state.player_list.clone()); - - if let Some(id) = &server.state.player_list { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - - client.set_game_mode(GameMode::Creative); - client.send_message("Welcome to Valence! Build something cool.".italic()); - } - - let player = server.entities.get_mut(client.entity_id).unwrap(); - - while let Some(event) = client.next_event() { - event.handle_default(client, player); - match event { - ClientEvent::StartDigging { position, .. } => { - // Allows clients in creative mode to break blocks. - if client.game_mode() == GameMode::Creative { - world.chunks.set_block_state(position, BlockState::AIR); - } - } - ClientEvent::FinishDigging { position, .. } => { - // Allows clients in survival mode to break blocks. - world.chunks.set_block_state(position, BlockState::AIR); - } - ClientEvent::UseItemOnBlock { .. } => { - // TODO: reimplement when inventories are re-added. - /* - if hand == Hand::Main { - if let Some(stack) = client.held_item() { - if let Some(held_block_kind) = stack.item.to_block_kind() { - let block_to_place = BlockState::from_kind(held_block_kind); - - if client.game_mode() == GameMode::Creative - || client.consume_held_item(1).is_ok() - { - if world - .chunks - .block_state(position) - .map(|s| s.is_replaceable()) - .unwrap_or(false) - { - world.chunks.set_block_state(position, block_to_place); - } else { - let place_at = position.get_in_direction(face); - world.chunks.set_block_state(place_at, block_to_place); - } - } - } - } - } - */ - } - _ => {} - } - } - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - player.set_deleted(true); - if let Some(id) = &server.state.player_list { - server.player_lists[id].remove(client.uuid()); - } - return false; - } - - if client.position().y <= -20.0 { - client.teleport(spawn_pos, client.yaw(), client.pitch()); - } - - true +fn toggle_gamemode_on_sneak( + mut clients: Query<&mut Client>, + mut events: EventReader, +) { + for event in events.iter() { + let Ok(mut client) = clients.get_component_mut::(event.client) else { + continue; + }; + let mode = client.game_mode(); + client.set_game_mode(match mode { + GameMode::Survival => GameMode::Creative, + GameMode::Creative => GameMode::Survival, + _ => GameMode::Creative, }); } } + +fn digging_creative_mode( + clients: Query<&Client>, + mut instances: Query<&mut Instance>, + mut events: EventReader, +) { + let mut instance = instances.single_mut(); + + for event in events.iter() { + let Ok(client) = clients.get_component::(event.client) else { + continue; + }; + if client.game_mode() == GameMode::Creative { + instance.set_block_state(event.position, BlockState::AIR); + } + } +} + +fn digging_survival_mode( + clients: Query<&Client>, + mut instances: Query<&mut Instance>, + mut events: EventReader, +) { + let mut instance = instances.single_mut(); + + for event in events.iter() { + let Ok(client) = clients.get_component::(event.client) else { + continue; + }; + if client.game_mode() == GameMode::Survival { + instance.set_block_state(event.position, BlockState::AIR); + } + } +} + +fn place_blocks( + mut clients: Query<(&Client, &mut Inventory)>, + mut instances: Query<&mut Instance>, + mut events: EventReader, +) { + let mut instance = instances.single_mut(); + + for event in events.iter() { + let Ok((client, mut inventory)) = clients.get_mut(event.client) else { + continue; + }; + if event.hand != Hand::Main { + continue; + } + + // get the held item + let slot_id = client.held_item_slot(); + let Some(stack) = inventory.slot(slot_id) else { + // no item in the slot + continue; + }; + + let Some(block_kind) = stack.item.to_block_kind() else { + // can't place this item as a block + continue; + }; + + if client.game_mode() == GameMode::Survival { + // check if the player has the item in their inventory and remove + // it. + let slot = if stack.count() > 1 { + let mut stack = stack.clone(); + stack.set_count(stack.count() - 1); + Some(stack) + } else { + None + }; + inventory.replace_slot(slot_id, slot); + } + let real_pos = event.position.get_in_direction(event.face); + instance.set_block_state(real_pos, block_kind.to_state()); + } +} diff --git a/crates/valence/examples/chest.rs b/crates/valence/examples/chest.rs index 53ca907..d050df8 100644 --- a/crates/valence/examples/chest.rs +++ b/crates/valence/examples/chest.rs @@ -1,268 +1,100 @@ -pub fn main() { - todo!("reimplement when inventories are re-added"); -} - -/* -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use num::Integer; +use tracing::warn; +use valence::client::despawn_disconnected_clients; +use valence::client::event::{default_event_handler, StartSneaking, UseItemOnBlock}; use valence::prelude::*; -use valence::protocol::VarInt; -pub fn main() -> ShutdownResult { +const SPAWN_Y: i32 = 64; +const CHEST_POS: [i32; 3] = [0, SPAWN_Y + 1, 3]; + +pub fn main() { tracing_subscriber::fmt().init(); - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - ServerState { - player_list: None, - chest: Default::default(), - tick: 0, - }, - ) + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_to_stage(EventLoop, toggle_gamemode_on_sneak) + .add_system_to_stage(EventLoop, open_chest) + .add_system_set(PlayerList::default_system_set()) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .run(); } -struct Game { - player_count: AtomicUsize, -} +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); -struct ServerState { - player_list: Option, - chest: InventoryId, - tick: u32, -} - -#[derive(Default)] -struct ClientState { - entity_id: EntityId, - // open_inventory: Option, -} - -const MAX_PLAYERS: usize = 10; - -const SIZE_X: usize = 100; -const SIZE_Z: usize = 100; - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - fn dimensions(&self) -> Vec { - vec![Dimension { - fixed_time: Some(6000), - ..Dimension::default() - }] - } - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some(include_bytes!("../assets/logo-64x64.png").as_slice().into()), + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); } } - fn init(&self, server: &mut Server) { - let world = server.worlds.insert(DimensionId::default(), ()).1; - server.state.player_list = Some(server.player_lists.insert(()).0); - - // initialize chunks - for chunk_z in -2..Integer::div_ceil(&(SIZE_Z as i32), &16) + 2 { - for chunk_x in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 { - world.chunks.insert( - [chunk_x, chunk_z], - UnloadedChunk::default(), - (), - ); - } + for z in -25..25 { + for x in -25..25 { + instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } - - // initialize blocks in the chunks - for x in 0..SIZE_X { - for z in 0..SIZE_Z { - world - .chunks - .set_block_state((x as i32, 0, z as i32), BlockState::GRASS_BLOCK); - } - } - - world.chunks.set_block_state((50, 0, 54), BlockState::STONE); - world.chunks.set_block_state((50, 1, 54), BlockState::CHEST); - - // create chest inventory - let inv = ConfigurableInventory::new(27, VarInt(2), None); - let title = "Extra".italic() - + " Chesty".not_italic().bold().color(Color::RED) - + " Chest".not_italic(); - - let (id, _inv) = server.inventories.insert(inv, title, ()); - server.state.chest = id; } + instance.set_block_state(CHEST_POS, BlockState::CHEST); + instance.set_block_state( + [CHEST_POS[0], CHEST_POS[1] - 1, CHEST_POS[2]], + BlockState::STONE, + ); - fn update(&self, server: &mut Server) { - server.state.tick += 1; - if server.state.tick > 10 { - server.state.tick = 0; - } - let (world_id, world) = server.worlds.iter_mut().next().unwrap(); + world.spawn(instance); - let spawn_pos = [SIZE_X as f64 / 2.0, 1.0, SIZE_Z as f64 / 2.0]; + let inventory = Inventory::with_title( + InventoryKind::Generic9x3, + "Extra".italic() + " Chesty".not_italic().bold().color(Color::RED) + " Chest".not_italic(), + ); + world.spawn(inventory); +} - if let Some(inv) = server.inventories.get_mut(server.state.chest) { - if server.state.tick == 0 { - rotate_items(inv); - } - } +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + for mut client in &mut clients { + client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + client.set_instance(instances.single()); + client.set_game_mode(GameMode::Creative); + } +} - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } - - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, entity)) => { - entity.set_world(world_id); - client.state.entity_id = id - } - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } - - client.respawn(world_id); - client.set_flat(true); - client.teleport(spawn_pos, 0.0, 0.0); - client.set_player_list(server.state.player_list.clone()); - - if let Some(id) = &server.state.player_list { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - ); - } - - client.send_message("Welcome to Valence! Sneak to give yourself an item.".italic()); - } - - let player = server.entities.get_mut(client.state.entity_id).unwrap(); - - while let Some(event) = client.next_event() { - event.handle_default(client, player); - match event { - ClientEvent::UseItemOnBlock { hand, position, .. } => { - if hand == Hand::Main - && world.chunks.block_state(position) == Some(BlockState::CHEST) - { - client.send_message("Opening chest!"); - client.open_inventory(server.state.chest); - } - } - ClientEvent::CloseScreen { window_id } => { - if window_id > 0 { - client.send_message(format!("Window closed: {}", window_id)); - client.send_message(format!("Chest: {:?}", server.state.chest)); - } - } - ClientEvent::ClickContainer { - window_id, - state_id, - slot_id, - mode, - slot_changes, - carried_item, - .. - } => { - println!( - "window_id: {:?}, state_id: {:?}, slot_id: {:?}, mode: {:?}, \ - slot_changes: {:?}, carried_item: {:?}", - window_id, state_id, slot_id, mode, slot_changes, carried_item - ); - client.cursor_held_item = carried_item; - if let Some(window) = client.open_inventory.as_mut() { - if let Some(obj_inv) = - server.inventories.get_mut(window.object_inventory) - { - for (slot_id, slot) in slot_changes { - if slot_id < obj_inv.slot_count() as SlotId { - obj_inv.set_slot(slot_id, slot); - } else { - let offset = obj_inv.slot_count() as SlotId; - client.inventory.set_slot( - slot_id - offset + PlayerInventory::GENERAL_SLOTS.start, - slot, - ); - } - } - } - } - } - ClientEvent::StartSneaking => { - let slot_id: SlotId = PlayerInventory::HOTBAR_SLOTS.start; - let stack = match client.inventory.slot(slot_id) { - None => ItemStack::new(ItemKind::Stone, 1, None), - Some(s) => ItemStack::new(s.item, s.count() + 1, None), - }; - client.inventory.set_slot(slot_id, Some(stack)); - } - _ => {} - } - } - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities.remove(client.state.entity_id); - if let Some(id) = &server.state.player_list { - server.player_lists[id].remove(client.uuid()); - } - return false; - } - - if client.position().y <= -20.0 { - client.teleport(spawn_pos, client.yaw(), client.pitch()); - } - - true +fn toggle_gamemode_on_sneak( + mut clients: Query<&mut Client>, + mut events: EventReader, +) { + for event in events.iter() { + let Ok(mut client) = clients.get_component_mut::(event.client) else { + continue; + }; + let mode = client.game_mode(); + client.set_game_mode(match mode { + GameMode::Survival => GameMode::Creative, + GameMode::Creative => GameMode::Survival, + _ => GameMode::Creative, }); } } -fn rotate_items(inv: &mut ConfigurableInventory) { - for i in 1..inv.slot_count() { - let a = inv.slot((i - 1) as SlotId); - let b = inv.set_slot(i as SlotId, a.cloned()); - inv.set_slot((i - 1) as SlotId, b); +fn open_chest( + mut commands: Commands, + inventories: Query, Without)>, + mut events: EventReader, +) { + let Ok(inventory) = inventories.get_single() else { + warn!("No inventories"); + return; + }; + + for event in events.iter() { + if event.position != CHEST_POS.into() { + continue; + } + let open_inventory = OpenInventory::new(inventory); + commands.entity(event.client).insert(open_inventory); } } -*/ diff --git a/crates/valence/examples/combat.rs b/crates/valence/examples/combat.rs index 77311ea..bd578d2 100644 --- a/crates/valence/examples/combat.rs +++ b/crates/valence/examples/combat.rs @@ -1,252 +1,166 @@ -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; - +use glam::Vec3Swizzles; +use valence::client::despawn_disconnected_clients; +use valence::client::event::{ + default_event_handler, InteractWithEntity, StartSprinting, StopSprinting, +}; use valence::prelude::*; -pub fn main() -> ShutdownResult { +const SPAWN_Y: i32 = 64; +const ARENA_RADIUS: i32 = 32; + +/// Attached to every client. +#[derive(Component)] +struct CombatState { + /// The tick the client was last attacked. + last_attacked_tick: i64, + has_bonus_knockback: bool, +} + +pub fn main() { tracing_subscriber::fmt().init(); - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - None, - ) + App::new() + .add_plugin(ServerPlugin::new(())) + .add_startup_system(setup) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_to_stage(EventLoop, handle_combat_events) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .add_system_set(PlayerList::default_system_set()) + .add_system(teleport_oob_clients) + .run(); } -struct Game { - player_count: AtomicUsize, -} +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); -#[derive(Default)] -struct ClientState { - /// The client's player entity. - player: EntityId, - /// The extra knockback on the first hit while sprinting. - extra_knockback: bool, -} - -#[derive(Default)] -struct EntityState { - client: ClientId, - attacked: bool, - attacker_pos: Vec3, - extra_knockback: bool, - last_attack_time: Ticks, -} - -const MAX_PLAYERS: usize = 10; - -const SPAWN_POS: BlockPos = BlockPos::new(0, 20, 0); - -#[async_trait] -impl Config for Game { - type ServerState = Option; - type ClientState = ClientState; - type EntityState = EntityState; - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); } } - fn init(&self, server: &mut Server) { - let (_, world) = server.worlds.insert(DimensionId::default(), ()); - server.state = Some(server.player_lists.insert(()).0); + // Create circular arena. + for z in -ARENA_RADIUS..ARENA_RADIUS { + for x in -ARENA_RADIUS..ARENA_RADIUS { + let dist = f64::hypot(x as _, z as _) / ARENA_RADIUS as f64; - let min_y = world.chunks.min_y(); - let height = world.chunks.height(); + if dist > 1.0 { + continue; + } - // Create circular arena. - let size = 2; - for chunk_z in -size - 2..size + 2 { - for chunk_x in -size - 2..size + 2 { - let mut chunk = UnloadedChunk::new(height); + let block = if rand::random::() < dist { + BlockState::STONE + } else { + BlockState::DEEPSLATE + }; - let r = -size..size; - if r.contains(&chunk_x) && r.contains(&chunk_z) { - for z in 0..16 { - for x in 0..16 { - let block_x = chunk_x * 16 + x as i32; - let block_z = chunk_z * 16 + z as i32; - if f64::hypot(block_x as f64, block_z as f64) <= size as f64 * 16.0 { - for y in 0..(SPAWN_POS.y - min_y + 1) as usize { - chunk.set_block_state(x, y, z, BlockState::STONE); - } - } - } - } - } - - world.chunks.insert([chunk_x, chunk_z], chunk, ()); + for y in 0..SPAWN_Y { + instance.set_block_state([x, y, z], block); } } - - world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK); } - fn update(&self, server: &mut Server) { - let current_tick = server.current_tick(); - let (world_id, _) = server.worlds.iter_mut().next().unwrap(); + world.spawn(instance); +} - server.clients.retain(|client_id, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } +fn init_clients( + mut commands: Commands, + mut clients: Query<(Entity, &mut Client), Added>, + instances: Query>, +) { + let instance = instances.single(); - let (player_id, player) = match server.entities.insert_with_uuid( - EntityKind::Player, - client.uuid(), - EntityState::default(), - ) { - Some(e) => e, - None => { - client.disconnect("Conflicting UUID"); - return false; - } - }; + for (entity, mut client) in &mut clients { + client.set_position([0.0, SPAWN_Y as f64, 0.0]); + client.set_instance(instance); - player.set_world(world_id); - player.client = client_id; + commands.entity(entity).insert(( + CombatState { + last_attacked_tick: 0, + has_bonus_knockback: false, + }, + McEntity::with_uuid(EntityKind::Player, instance, client.uuid()), + )); + } +} - client.player = player_id; +fn handle_combat_events( + manager: Res, + server: Res, + mut start_sprinting: EventReader, + mut stop_sprinting: EventReader, + mut interact_with_entity: EventReader, + mut clients: Query<(&mut Client, &mut CombatState, &mut McEntity)>, +) { + for &StartSprinting { client } in start_sprinting.iter() { + if let Ok((_, mut state, _)) = clients.get_mut(client) { + state.has_bonus_knockback = true; + } + } - client.respawn(world_id); - client.set_flat(true); - client.set_game_mode(GameMode::Survival); - client.teleport( - [ - SPAWN_POS.x as f64 + 0.5, - SPAWN_POS.y as f64 + 1.0, - SPAWN_POS.z as f64 + 0.5, - ], - 0.0, - 0.0, - ); - client.set_player_list(server.state.clone()); + for &StopSprinting { client } in stop_sprinting.iter() { + if let Ok((_, mut state, _)) = clients.get_mut(client) { + state.has_bonus_knockback = false; + } + } - if let Some(id) = &server.state { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } + for &InteractWithEntity { + client: attacker_client, + entity_id, + .. + } in interact_with_entity.iter() + { + let Some(victim_client) = manager.get_with_protocol_id(entity_id) else { + // Attacked entity doesn't exist. + continue + }; - client.send_message("Welcome to the arena.".italic()); - if self.player_count.load(Ordering::SeqCst) <= 1 { - client.send_message("Have another player join the game with you.".italic()); - } - } + let Ok([(attacker_client, mut attacker_state, _), (mut victim_client, mut victim_state, mut victim_entity)]) = + clients.get_many_mut([attacker_client, victim_client]) + else { + // Victim or attacker does not exist, or the attacker is attacking itself. + continue + }; - while let Some(event) = client.next_event() { - let player = server - .entities - .get_mut(client.player) - .expect("missing player entity"); + if server.current_tick() - victim_state.last_attacked_tick < 10 { + // Victim is still on attack cooldown. + continue; + } - event.handle_default(client, player); - match event { - ClientEvent::StartSprinting => { - client.extra_knockback = true; - } - ClientEvent::StopSprinting => { - client.extra_knockback = false; - } - ClientEvent::InteractWithEntity { entity_id, .. } => { - if let Some((id, target)) = server.entities.get_with_raw_id_mut(entity_id) { - if !target.attacked - && current_tick - target.last_attack_time >= 10 - && id != client.player - { - target.attacked = true; - target.attacker_pos = client.position(); - target.extra_knockback = client.extra_knockback; - target.last_attack_time = current_tick; + victim_state.last_attacked_tick = server.current_tick(); - client.extra_knockback = false; - } - } - } - _ => {} - } - } + let victim_pos = victim_client.position().xz(); + let attacker_pos = attacker_client.position().xz(); - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities[client.player].set_deleted(true); - if let Some(id) = &server.state { - server.player_lists[id].remove(client.uuid()); - } - return false; - } + let dir = (victim_pos - attacker_pos).normalize().as_vec2(); - if client.position().y <= 0.0 { - client.teleport( - [ - SPAWN_POS.x as f64 + 0.5, - SPAWN_POS.y as f64 + 1.0, - SPAWN_POS.z as f64 + 0.5, - ], - client.yaw(), - client.pitch(), - ); - } + let knockback_xz = if attacker_state.has_bonus_knockback { + 18.0 + } else { + 8.0 + }; + let knockback_y = if attacker_state.has_bonus_knockback { + 8.432 + } else { + 6.432 + }; - true - }); + victim_client.set_velocity([dir.x * knockback_xz, knockback_y, dir.y * knockback_xz]); - for (_, entity) in server.entities.iter_mut() { - if entity.attacked { - entity.attacked = false; - if let Some(victim) = server.clients.get_mut(entity.client) { - let victim_pos = Vec2::new(victim.position().x, victim.position().z); - let attacker_pos = Vec2::new(entity.attacker_pos.x, entity.attacker_pos.z); + attacker_state.has_bonus_knockback = false; - let dir = (victim_pos - attacker_pos).normalized(); + victim_client.trigger_status(EntityStatus::DamageFromGenericSource); + victim_entity.trigger_status(EntityStatus::DamageFromGenericSource); + } +} - let knockback_xz = if entity.extra_knockback { 18.0 } else { 8.0 }; - let knockback_y = if entity.extra_knockback { 8.432 } else { 6.432 }; - - let vel = Vec3::new(dir.x * knockback_xz, knockback_y, dir.y * knockback_xz); - victim.set_velocity(vel.as_()); - - entity.push_event(EntityEvent::DamageFromGenericSource); - entity.push_event(EntityEvent::Damage); - victim.send_entity_event(EntityEvent::DamageFromGenericSource); - victim.send_entity_event(EntityEvent::Damage); - } - } +fn teleport_oob_clients(mut clients: Query<&mut Client>) { + for mut client in &mut clients { + if client.position().y < 0.0 { + client.set_position([0.0, SPAWN_Y as _, 0.0]); } } } diff --git a/crates/valence/examples/conway.rs b/crates/valence/examples/conway.rs index aae3263..308bcbe 100644 --- a/crates/valence/examples/conway.rs +++ b/crates/valence/examples/conway.rs @@ -1,297 +1,206 @@ use std::mem; -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; -use num::Integer; -use rayon::prelude::*; +use valence::client::despawn_disconnected_clients; +use valence::client::event::{default_event_handler, StartDigging, StartSneaking}; use valence::prelude::*; -pub fn main() -> ShutdownResult { +const BOARD_MIN_X: i32 = -30; +const BOARD_MAX_X: i32 = 30; +const BOARD_MIN_Z: i32 = -30; +const BOARD_MAX_Z: i32 = 30; +const BOARD_Y: i32 = 64; + +const BOARD_SIZE_X: usize = (BOARD_MAX_X - BOARD_MIN_X + 1) as usize; +const BOARD_SIZE_Z: usize = (BOARD_MAX_Z - BOARD_MIN_Z + 1) as usize; + +const SPAWN_POS: DVec3 = DVec3::new( + (BOARD_MIN_X + BOARD_MAX_X) as f64 / 2.0, + BOARD_Y as f64 + 1.0, + (BOARD_MIN_Z + BOARD_MAX_Z) as f64 / 2.0, +); + +pub fn main() { tracing_subscriber::fmt().init(); - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - ServerState { - player_list: None, - paused: false, - board: vec![false; SIZE_X * SIZE_Z].into_boxed_slice(), - board_buf: vec![false; SIZE_X * SIZE_Z].into_boxed_slice(), - }, - ) + App::new() + .add_plugin(ServerPlugin::new(()).with_biomes(vec![Biome { + grass_color: Some(0x00ff00), + ..Default::default() + }])) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_set(PlayerList::default_system_set()) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .add_system_to_stage(EventLoop, toggle_cell_on_dig) + .add_system(update_board) + .add_system(pause_on_crouch) + .add_system(reset_oob_clients) + .run(); } -struct Game { - player_count: AtomicUsize, +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); + + for z in -10..10 { + for x in -10..10 { + instance.insert_chunk([x, z], Chunk::default()); + } + } + + for z in BOARD_MIN_Z..=BOARD_MAX_Z { + for x in BOARD_MIN_X..=BOARD_MAX_X { + instance.set_block_state([x, BOARD_Y, z], BlockState::DIRT); + } + } + + world.spawn(instance); + + world.insert_resource(LifeBoard { + paused: true, + board: vec![false; BOARD_SIZE_X * BOARD_SIZE_Z].into(), + board_buf: vec![false; BOARD_SIZE_X * BOARD_SIZE_Z].into(), + }); } -struct ServerState { - player_list: Option, - paused: bool, +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + for mut client in &mut clients { + client.set_position(SPAWN_POS); + client.set_instance(instances.single()); + client.set_game_mode(GameMode::Survival); + + client.send_message("Welcome to Conway's game of life in Minecraft!".italic()); + client.send_message( + "Sneak to toggle running the simulation and the left mouse button to bring blocks to \ + life." + .italic(), + ); + } +} + +#[derive(Resource)] +struct LifeBoard { + pub paused: bool, board: Box<[bool]>, board_buf: Box<[bool]>, } -#[derive(Default)] -struct ClientState { - entity_id: EntityId, -} +impl LifeBoard { + pub fn get(&self, x: i32, z: i32) -> bool { + if (BOARD_MIN_X..=BOARD_MAX_X).contains(&x) && (BOARD_MIN_Z..=BOARD_MAX_Z).contains(&z) { + let x = (x - BOARD_MIN_X) as usize; + let z = (z - BOARD_MIN_Z) as usize; -const MAX_PLAYERS: usize = 10; - -const SIZE_X: usize = 100; -const SIZE_Z: usize = 100; -const BOARD_Y: i32 = 50; - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - fn dimensions(&self) -> Vec { - vec![Dimension { - fixed_time: Some(6000), - ..Dimension::default() - }] - } - - fn biomes(&self) -> Vec { - vec![Biome { - name: ident!("plains"), - grass_color: Some(0x00ff00), - ..Biome::default() - }] - } - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), + self.board[x + z * BOARD_SIZE_X] + } else { + false } } - fn init(&self, server: &mut Server) { - let world = server.worlds.insert(DimensionId::default(), ()).1; - server.state.player_list = Some(server.player_lists.insert(()).0); + pub fn set(&mut self, x: i32, z: i32, value: bool) { + if (BOARD_MIN_X..=BOARD_MAX_X).contains(&x) && (BOARD_MIN_Z..=BOARD_MAX_Z).contains(&z) { + let x = (x - BOARD_MIN_X) as usize; + let z = (z - BOARD_MIN_Z) as usize; - for chunk_z in -2..Integer::div_ceil(&(SIZE_Z as i32), &16) + 2 { - for chunk_x in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 { - world - .chunks - .insert([chunk_x, chunk_z], UnloadedChunk::default(), ()); - } + self.board[x + z * BOARD_SIZE_X] = value; } } - fn update(&self, server: &mut Server) { - let current_tick = server.current_tick(); - let (world_id, world) = server.worlds.iter_mut().next().unwrap(); + pub fn update(&mut self) { + for (idx, cell) in self.board_buf.iter_mut().enumerate() { + let x = (idx % BOARD_SIZE_X) as i32; + let z = (idx / BOARD_SIZE_X) as i32; - let spawn_pos = [ - SIZE_X as f64 / 2.0, - BOARD_Y as f64 + 1.0, - SIZE_Z as f64 / 2.0, - ]; + let mut live_neighbors = 0; - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } + for cz in z - 1..=z + 1 { + for cx in x - 1..=x + 1 { + if !(cx == x && cz == z) { + let idx = cx.rem_euclid(BOARD_SIZE_X as i32) as usize + + cz.rem_euclid(BOARD_SIZE_Z as i32) as usize * BOARD_SIZE_X; - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, entity)) => { - entity.set_world(world_id); - client.entity_id = id - } - None => { - client.disconnect("Conflicting UUID"); - return false; + live_neighbors += self.board[idx] as i32; } } - - client.respawn(world_id); - client.set_flat(true); - client.teleport(spawn_pos, 0.0, 0.0); - client.set_player_list(server.state.player_list.clone()); - - if let Some(id) = &server.state.player_list { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - - client.send_message("Welcome to Conway's game of life in Minecraft!".italic()); - client.send_message( - "Sneak and hold the left mouse button to bring blocks to life.".italic(), - ); } - let player = server.entities.get_mut(client.entity_id).unwrap(); - - while let Some(event) = client.next_event() { - event.handle_default(client, player); - match event { - ClientEvent::StartDigging { position, .. } => { - if (0..SIZE_X as i32).contains(&position.x) - && (0..SIZE_Z as i32).contains(&position.z) - && position.y == BOARD_Y - { - let index = position.x as usize + position.z as usize * SIZE_X; - - if !server.state.board[index] { - // client.play_sound( - // Ident::new("minecraft:block.note_block. - // banjo").unwrap(), - // SoundCategory::Block, - // Vec3::new(position.x, position.y, - // position.z).as_(), - // 0.5f32, - // 1f32, - // ); - } - - server.state.board[index] = true; - } - } - ClientEvent::UseItemOnBlock { hand, .. } => { - if hand == Hand::Main { - client.send_message("I said left click, not right click!".italic()); - } - } - _ => {} - } - } - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - player.set_deleted(true); - if let Some(id) = &server.state.player_list { - server.player_lists[id].remove(client.uuid()); - } - return false; - } - - if client.position().y <= 0.0 { - client.teleport(spawn_pos, client.yaw(), client.pitch()); - server.state.board.fill(false); - } - - if let TrackedData::Player(data) = player.data() { - let sneaking = data.get_pose() == Pose::Sneaking; - if sneaking != server.state.paused { - server.state.paused = sneaking; - // client.play_sound( - // Ident::new("block.note_block.pling").unwrap(), - // SoundCategory::Block, - // client.position(), - // 0.5, - // if sneaking { 0.5 } else { 1.0 }, - // ); - } - } - - // Display Playing in green or Paused in red - client.set_action_bar(if server.state.paused { - "Paused".color(Color::RED) + let live = self.board[idx]; + if live { + *cell = (2..=3).contains(&live_neighbors); } else { - "Playing".color(Color::GREEN) - }); - - true - }); - - if !server.state.paused && current_tick % 2 == 0 { - server - .state - .board_buf - .par_iter_mut() - .enumerate() - .for_each(|(i, cell)| { - let cx = (i % SIZE_X) as i32; - let cz = (i / SIZE_Z) as i32; - - let mut live_count = 0; - for z in cz - 1..=cz + 1 { - for x in cx - 1..=cx + 1 { - if !(x == cx && z == cz) { - let i = x.rem_euclid(SIZE_X as i32) as usize - + z.rem_euclid(SIZE_Z as i32) as usize * SIZE_X; - if server.state.board[i] { - live_count += 1; - } - } - } - } - - if server.state.board[cx as usize + cz as usize * SIZE_X] { - *cell = (2..=3).contains(&live_count); - } else { - *cell = live_count == 3; - } - }); - - mem::swap(&mut server.state.board, &mut server.state.board_buf); + *cell = live_neighbors == 3; + } } - let min_y = world.chunks.min_y(); + mem::swap(&mut self.board, &mut self.board_buf); + } - for chunk_x in 0..Integer::div_ceil(&SIZE_X, &16) { - for chunk_z in 0..Integer::div_ceil(&SIZE_Z, &16) { - let chunk = world - .chunks - .get_mut([chunk_x as i32, chunk_z as i32]) - .unwrap(); - for x in 0..16 { - for z in 0..16 { - let cell_x = chunk_x * 16 + x; - let cell_z = chunk_z * 16 + z; + pub fn clear(&mut self) { + self.board.fill(false); + } +} - if cell_x < SIZE_X && cell_z < SIZE_Z { - let b = if server.state.board[cell_x + cell_z * SIZE_X] { - BlockState::GRASS_BLOCK - } else { - BlockState::DIRT - }; - chunk.set_block_state(x, (BOARD_Y - min_y) as usize, z, b); - } - } - } +fn toggle_cell_on_dig(mut events: EventReader, mut board: ResMut) { + for event in events.iter() { + let (x, z) = (event.position.x, event.position.z); + + let live = board.get(x, z); + board.set(x, z, !live); + } +} + +fn update_board( + mut board: ResMut, + mut instances: Query<&mut Instance>, + server: Res, +) { + if !board.paused && server.current_tick() % 2 == 0 { + board.update(); + } + + let mut instance = instances.single_mut(); + + for z in BOARD_MIN_Z..=BOARD_MAX_Z { + for x in BOARD_MIN_X..=BOARD_MAX_X { + let block = if board.get(x, z) { + BlockState::GRASS_BLOCK + } else { + BlockState::DIRT + }; + + instance.set_block_state([x, BOARD_Y, z], block); + } + } +} + +fn pause_on_crouch( + mut events: EventReader, + mut board: ResMut, + mut clients: Query<&mut Client>, +) { + for _ in events.iter() { + board.paused = !board.paused; + + for mut client in clients.iter_mut() { + if board.paused { + client.set_action_bar("Paused".italic().color(Color::RED)); + } else { + client.set_action_bar("Playing".italic().color(Color::GREEN)); } } } } + +fn reset_oob_clients(mut clients: Query<&mut Client>, mut board: ResMut) { + for mut client in &mut clients { + if client.position().y < 0.0 { + client.set_position(SPAWN_POS); + board.clear(); + } + } +} diff --git a/crates/valence/examples/cow_sphere.rs b/crates/valence/examples/cow_sphere.rs index df5a787..1c88c16 100644 --- a/crates/valence/examples/cow_sphere.rs +++ b/crates/valence/examples/cow_sphere.rs @@ -1,222 +1,100 @@ -use std::borrow::Cow; use std::f64::consts::TAU; -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; +use glam::{DQuat, EulerRot}; +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; +use valence::math::to_yaw_and_pitch; use valence::prelude::*; -pub fn main() -> ShutdownResult { +const SPHERE_CENTER: DVec3 = DVec3::new(0.5, SPAWN_POS.y as f64 + 2.0, 0.5); +const SPHERE_AMOUNT: usize = 200; +const SPHERE_KIND: EntityKind = EntityKind::Cow; +const SPHERE_MIN_RADIUS: f64 = 6.0; +const SPHERE_MAX_RADIUS: f64 = 12.0; +const SPHERE_FREQ: f64 = 0.5; + +const SPAWN_POS: BlockPos = BlockPos::new(0, 100, -16); + +/// Marker component for entities that are part of the sphere. +#[derive(Component)] +struct SpherePart; + +fn main() { tracing_subscriber::fmt().init(); - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - ServerState { - player_list: None, - cows: vec![], - }, - ) + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(update_sphere) + .add_system(despawn_disconnected_clients) + .add_system_set(PlayerList::default_system_set()) + .run(); } -struct Game { - player_count: AtomicUsize, -} +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); -struct ServerState { - player_list: Option, - cows: Vec, -} - -#[derive(Default)] -struct ClientState { - entity_id: EntityId, -} - -const MAX_PLAYERS: usize = 10; - -const SPAWN_POS: BlockPos = BlockPos::new(0, 100, -25); - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - const SAMPLE: &[PlayerSampleEntry] = &[ - PlayerSampleEntry { - name: Cow::Borrowed("§cFirst Entry"), - id: Uuid::nil(), - }, - PlayerSampleEntry { - name: Cow::Borrowed("§6§oSecond Entry"), - id: Uuid::nil(), - }, - ]; - - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: SAMPLE.into(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); } } - fn init(&self, server: &mut Server) { - let (world_id, world) = server.worlds.insert(DimensionId::default(), ()); - server.state.player_list = Some(server.player_lists.insert(()).0); + instance.set_block_state(SPAWN_POS, BlockState::BEDROCK); - let size = 5; - for z in -size..size { - for x in -size..size { - world.chunks.insert([x, z], UnloadedChunk::default(), ()); - } - } + let instance_id = world.spawn(instance).id(); - world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK); + world.spawn_batch( + [0; SPHERE_AMOUNT].map(|_| (McEntity::new(SPHERE_KIND, instance_id), SpherePart)), + ); +} - server.state.cows.extend((0..200).map(|_| { - let (id, e) = server.entities.insert(EntityKind::Cow, ()); - e.set_world(world_id); - id - })); +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + for mut client in &mut clients { + client.set_position([ + SPAWN_POS.x as f64 + 0.5, + SPAWN_POS.y as f64 + 1.0, + SPAWN_POS.z as f64 + 0.5, + ]); + client.set_instance(instances.single()); + client.set_game_mode(GameMode::Creative); } +} - fn update(&self, server: &mut Server) { - let current_tick = server.current_tick(); - let (world_id, _) = server.worlds.iter_mut().next().expect("missing world"); +fn update_sphere(server: Res, mut parts: Query<&mut McEntity, With>) { + let time = server.current_tick() as f64 / server.tps() as f64; - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } + let rot_angles = DVec3::new(0.2, 0.4, 0.6) * SPHERE_FREQ * time * TAU % TAU; + let rot = DQuat::from_euler(EulerRot::XYZ, rot_angles.x, rot_angles.y, rot_angles.z); - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, entity)) => { - entity.set_world(world_id); - client.entity_id = id - } - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } + let radius = lerp( + SPHERE_MIN_RADIUS, + SPHERE_MAX_RADIUS, + ((time * SPHERE_FREQ * TAU).sin() + 1.0) / 2.0, + ); - client.respawn(world_id); - client.set_flat(true); - client.set_game_mode(GameMode::Creative); - client.teleport( - [ - SPAWN_POS.x as f64 + 0.5, - SPAWN_POS.y as f64 + 1.0, - SPAWN_POS.z as f64 + 0.5, - ], - 0.0, - 0.0, - ); - client.set_player_list(server.state.player_list.clone()); + for (mut entity, p) in parts.iter_mut().zip(fibonacci_spiral(SPHERE_AMOUNT)) { + debug_assert!(p.is_normalized()); - if let Some(id) = &server.state.player_list { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - } + let dir = rot * p; + let (yaw, pitch) = to_yaw_and_pitch(dir.as_vec3()); - let entity = &mut server.entities[client.entity_id]; - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - if let Some(id) = &server.state.player_list { - server.player_lists[id].remove(client.uuid()); - } - entity.set_deleted(true); - - return false; - } - - while let Some(event) = client.next_event() { - event.handle_default(client, entity); - } - - true - }); - - let time = current_tick as f64 / server.shared.tick_rate() as f64; - - let rot = Mat3::rotation_x(time * TAU * 0.1) - .rotated_y(time * TAU * 0.2) - .rotated_z(time * TAU * 0.3); - - let radius = 6.0 + ((time * TAU / 2.5).sin() + 1.0) / 2.0 * 10.0; - - let player_pos = server - .clients - .iter() - .next() - .map(|c| c.1.position()) - .unwrap_or_default(); - - // TODO: remove hardcoded eye pos. - let eye_pos = Vec3::new(player_pos.x, player_pos.y + 1.6, player_pos.z); - - for (cow_id, p) in server - .state - .cows - .iter() - .cloned() - .zip(fibonacci_spiral(server.state.cows.len())) - { - let cow = server.entities.get_mut(cow_id).expect("missing cow"); - let rotated = p * rot; - let transformed = rotated * radius + [0.5, SPAWN_POS.y as f64 + 2.0, 0.5]; - - let yaw = f32::atan2(rotated.z as f32, rotated.x as f32).to_degrees() - 90.0; - let (looking_yaw, looking_pitch) = - to_yaw_and_pitch((eye_pos - transformed).normalized()); - - cow.set_position(transformed); - cow.set_yaw(yaw); - cow.set_pitch(looking_pitch as f32); - cow.set_head_yaw(looking_yaw as f32); - } + entity.set_position(SPHERE_CENTER + dir * radius); + entity.set_yaw(yaw); + entity.set_head_yaw(yaw); + entity.set_pitch(pitch); } } /// Distributes N points on the surface of a unit sphere. -fn fibonacci_spiral(n: usize) -> impl Iterator> { +fn fibonacci_spiral(n: usize) -> impl Iterator { let golden_ratio = (1.0 + 5_f64.sqrt()) / 2.0; (0..n).map(move |i| { @@ -227,6 +105,10 @@ fn fibonacci_spiral(n: usize) -> impl Iterator> { // Map from unit square to unit sphere. let theta = x * TAU; let phi = (1.0 - 2.0 * y).acos(); - Vec3::new(theta.cos() * phi.sin(), theta.sin() * phi.sin(), phi.cos()) + DVec3::new(theta.cos() * phi.sin(), theta.sin() * phi.sin(), phi.cos()) }) } + +fn lerp(a: f64, b: f64, t: f64) -> f64 { + a * (1.0 - t) + b * t +} diff --git a/crates/valence/examples/death.rs b/crates/valence/examples/death.rs index 9f46bc0..e100d6a 100644 --- a/crates/valence/examples/death.rs +++ b/crates/valence/examples/death.rs @@ -1,332 +1,93 @@ -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; - +use tracing::warn; +use valence::client::despawn_disconnected_clients; +use valence::client::event::{default_event_handler, PerformRespawn, StartSneaking}; use valence::prelude::*; -pub fn main() -> ShutdownResult { +const SPAWN_Y: i32 = 64; + +pub fn main() { tracing_subscriber::fmt().init(); - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - ServerState::default(), - ) + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_to_stage(EventLoop, squat_and_die) + .add_system_to_stage(EventLoop, necromancy) + .add_system_set(PlayerList::default_system_set()) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .run(); } -struct Game { - player_count: AtomicUsize, -} +fn setup(world: &mut World) { + for block in [BlockState::GRASS_BLOCK, BlockState::DEEPSLATE] { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); -#[derive(Default)] -struct ClientState { - entity_id: EntityId, - // World and position to respawn at - respawn_location: (WorldId, Vec3), - // Anticheat measure - can_respawn: bool, -} - -#[derive(Default)] -struct ServerState { - player_list: Option, - first_world: WorldId, - second_world: WorldId, - third_world: WorldId, -} - -const MAX_PLAYERS: usize = 10; - -const FLOOR_Y: i32 = 64; -const PLATFORM_X: i32 = 20; -const PLATFORM_Z: i32 = 20; -const LEFT_DEATH_LINE: i32 = 16; -const RIGHT_DEATH_LINE: i32 = 4; - -const FIRST_WORLD_SPAWN_BLOCK: BlockPos = BlockPos::new(10, FLOOR_Y, 10); -const SECOND_WORLD_SPAWN_BLOCK: BlockPos = BlockPos::new(5, FLOOR_Y, 5); -const THIRD_WORLD_SPAWN_BLOCK: BlockPos = BlockPos::new(5, FLOOR_Y, 5); - -#[derive(Clone, Copy, PartialEq, Eq)] -enum WhichWorld { - First, - Second, - Third, -} - -// Returns position of player standing on `pos` block -fn block_pos_to_vec(pos: BlockPos) -> Vec3 { - Vec3::new( - (pos.x as f64) + 0.5, - (pos.y as f64) + 1.0, - (pos.z as f64) + 0.5, - ) -} - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - fn dimensions(&self) -> Vec { - vec![ - Dimension { - fixed_time: Some(6000), - ..Dimension::default() - }, - Dimension { - fixed_time: Some(19000), - ..Dimension::default() - }, - ] - } - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), - player_sample: Default::default(), + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); + } } - } - fn init(&self, server: &mut Server) { - // We created server with meaningless default state. - // Let's create three worlds and create new ServerState. - server.state = ServerState { - player_list: Some(server.player_lists.insert(()).0), - first_world: create_world(server, FIRST_WORLD_SPAWN_BLOCK, WhichWorld::First), - second_world: create_world(server, SECOND_WORLD_SPAWN_BLOCK, WhichWorld::Second), - third_world: create_world(server, THIRD_WORLD_SPAWN_BLOCK, WhichWorld::Third), + for z in -25..25 { + for x in -25..25 { + instance.set_block_state([x, SPAWN_Y, z], block); + } + } + + world.spawn(instance); + } +} + +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + let instance = instances.into_iter().next().unwrap(); + + for mut client in &mut clients { + client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + client.set_respawn_screen(true); + client.set_instance(instance); + client.send_message( + "Welcome to Valence! Press shift to die in the game (but not in real life).".italic(), + ); + } +} + +fn squat_and_die(mut clients: Query<&mut Client>, mut events: EventReader) { + for event in events.iter() { + let Ok(mut client) = clients.get_component_mut::(event.client) else { + warn!("Client {:?} not found", event.client); + continue; }; - } - fn update(&self, server: &mut Server) { - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } - - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, entity)) => { - entity.set_world(server.state.first_world); - client.entity_id = id - } - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } - - client.respawn_location = ( - server.state.first_world, - block_pos_to_vec(FIRST_WORLD_SPAWN_BLOCK), - ); - - // `set_spawn_position` is used for compass _only_ - client.set_spawn_position(FIRST_WORLD_SPAWN_BLOCK, 0.0); - - client.set_flat(true); - client.respawn(server.state.first_world); - client.teleport(client.respawn_location.1, 0.0, 0.0); - - client.set_player_list(server.state.player_list.clone()); - - server - .player_lists - .get_mut(server.state.player_list.as_ref().unwrap()) - .insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - - client.set_respawn_screen(true); - - client.send_message("Welcome to the death example!".italic()); - client.send_message("Step over the left line to die. :)"); - client.send_message("Step over the right line to die and respawn in second world."); - client.send_message("Jumping down kills you and spawns you in another dimension."); - client.send_message("Sneaking triggers game credits after which you respawn."); - } - - // TODO after inventory support is added, show interaction with compass. - - // Handling respawn locations - if !client.can_respawn { - if client.position().y < 0.0 { - client.can_respawn = true; - client.kill(None, "You fell"); - // You could have also killed the player with `Client::set_health_and_food`, - // however you cannot send a message to the death screen - // that way - if client.world() == server.state.third_world { - // Falling in third world gets you back to the first world - client.respawn_location = ( - server.state.first_world, - block_pos_to_vec(FIRST_WORLD_SPAWN_BLOCK), - ); - client.set_spawn_position(FIRST_WORLD_SPAWN_BLOCK, 0.0); - } else { - // falling in first and second world will cause player to spawn in third - // world - client.respawn_location = ( - server.state.third_world, - block_pos_to_vec(THIRD_WORLD_SPAWN_BLOCK), - ); - // This is for compass to point at - client.set_spawn_position(THIRD_WORLD_SPAWN_BLOCK, 0.0); - } - } - - // Death lanes in the first world - if client.world() == server.state.first_world { - let death_msg = "You shouldn't cross suspicious lines"; - - if client.position().x >= LEFT_DEATH_LINE as f64 { - // Client went to the left, he dies - client.can_respawn = true; - client.kill(None, death_msg); - } - - if client.position().x <= RIGHT_DEATH_LINE as f64 { - // Client went to the right, he dies and spawns in world2 - client.can_respawn = true; - client.kill(None, death_msg); - client.respawn_location = ( - server.state.second_world, - block_pos_to_vec(SECOND_WORLD_SPAWN_BLOCK), - ); - client.set_spawn_position(SECOND_WORLD_SPAWN_BLOCK, 0.0); - } - } - } - - let player = server.entities.get_mut(client.entity_id).unwrap(); - - while let Some(event) = client.next_event() { - event.handle_default(client, player); - match event { - ClientEvent::PerformRespawn => { - if !client.can_respawn { - client.disconnect("Unexpected PerformRespawn"); - return false; - } - // Let's respawn our player. `spawn` will load the world, but we are - // responsible for teleporting the player. - - // You can store respawn however you want, for example in `Client`'s state. - let spawn = client.respawn_location; - client.respawn(spawn.0); - player.set_world(spawn.0); - client.teleport(spawn.1, 0.0, 0.0); - client.can_respawn = false; - } - ClientEvent::StartSneaking => { - // Roll the credits, respawn after - client.can_respawn = true; - client.win_game(true); - } - _ => {} - } - } - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities[client.entity_id].set_deleted(true); - - if let Some(list) = client.player_list() { - server.player_lists.get_mut(list).remove(client.uuid()); - } - - return false; - } - - true - }); + client.kill(None, "Squatted too hard."); } } -// Boilerplate for creating world -fn create_world(server: &mut Server, spawn_pos: BlockPos, world_type: WhichWorld) -> WorldId { - let dimension = match world_type { - WhichWorld::First => server.shared.dimensions().next().unwrap(), - WhichWorld::Second => server.shared.dimensions().next().unwrap(), - WhichWorld::Third => server.shared.dimensions().nth(1).unwrap(), - }; - - let (world_id, world) = server.worlds.insert(dimension.0, ()); - - // Create chunks - for chunk_z in -3..3 { - for chunk_x in -3..3 { - world - .chunks - .insert([chunk_x, chunk_z], UnloadedChunk::default(), ()); - } +fn necromancy( + mut clients: Query<&mut Client>, + mut events: EventReader, + instances: Query>, +) { + for event in events.iter() { + let Ok(mut client) = clients.get_component_mut::(event.client) else { + continue; + }; + client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + client.set_velocity([0.0, 0.0, 0.0]); + client.set_yaw(0.0); + client.set_pitch(0.0); + // make the client respawn in another instance + let idx = instances + .iter() + .position(|i| i == client.instance()) + .unwrap(); + let count = instances.iter().count(); + client.set_instance(instances.into_iter().nth((idx + 1) % count).unwrap()); } - - // Create platform - let platform_block = match world_type { - WhichWorld::First => BlockState::END_STONE, - WhichWorld::Second => BlockState::AMETHYST_BLOCK, - WhichWorld::Third => BlockState::BLACKSTONE, - }; - - for z in 0..PLATFORM_Z { - for x in 0..PLATFORM_X { - world - .chunks - .set_block_state([x, FLOOR_Y, z], platform_block); - } - } - - // Set death lines - if world_type == WhichWorld::First { - for z in 0..PLATFORM_Z { - world - .chunks - .set_block_state([LEFT_DEATH_LINE, FLOOR_Y, z], BlockState::GOLD_BLOCK); - world - .chunks - .set_block_state([RIGHT_DEATH_LINE, FLOOR_Y, z], BlockState::DIAMOND_BLOCK); - } - } - - // Set spawn block - world - .chunks - .set_block_state(spawn_pos, BlockState::REDSTONE_BLOCK); - - world_id } diff --git a/crates/valence/examples/entity_raycast.rs b/crates/valence/examples/entity_raycast.rs deleted file mode 100644 index ce53d09..0000000 --- a/crates/valence/examples/entity_raycast.rs +++ /dev/null @@ -1,433 +0,0 @@ -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use valence::prelude::*; -use valence_protocol::entity_meta::{Facing, PaintingKind}; -use valence_spatial_index::bvh::Bvh; -use valence_spatial_index::{RaycastHit, SpatialIndex, WithAabb}; - -pub fn main() -> ShutdownResult { - tracing_subscriber::fmt().init(); - - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - ServerState { - player_list: None, - bvh: Bvh::new(), - world: WorldId::NULL, - }, - ) -} - -struct Game { - player_count: AtomicUsize, -} - -#[derive(Default)] -struct ClientState { - player: EntityId, - shulker_bullet: EntityId, -} - -const MAX_PLAYERS: usize = 10; - -const SPAWN_POS: BlockPos = BlockPos::new(0, 100, -5); - -const PLAYER_EYE_HEIGHT: f64 = 1.62; - -// TODO -// const PLAYER_SNEAKING_EYE_HEIGHT: f64 = 1.495; - -struct ServerState { - player_list: Option, - bvh: Bvh>, - world: WorldId, -} - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), - } - } - - fn init(&self, server: &mut Server) { - let (world_id, world) = server.worlds.insert(DimensionId::default(), ()); - server.state.world = world_id; - - let (player_list_id, player_list) = server.player_lists.insert(()); - server.state.player_list = Some(player_list_id); - - let size = 5; - for z in -size..size { - for x in -size..size { - world.chunks.insert([x, z], UnloadedChunk::default(), ()); - } - } - - world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK); - - // ==== Item Frames ==== // - let (_, e) = server.entities.insert(EntityKind::ItemFrame, ()); - if let TrackedData::ItemFrame(i) = e.data_mut() { - i.set_rotation(Facing::North as i32); - } - e.set_world(world_id); - e.set_position([2.0, 102.0, 0.0]); - - let (_, e) = server.entities.insert(EntityKind::ItemFrame, ()); - if let TrackedData::ItemFrame(i) = e.data_mut() { - i.set_rotation(Facing::East as i32); - } - e.set_world(world_id); - e.set_position([3.0, 102.0, 0.0]); - - let (_, e) = server.entities.insert(EntityKind::GlowItemFrame, ()); - if let TrackedData::GlowItemFrame(i) = e.data_mut() { - i.set_rotation(Facing::South as i32); - } - e.set_world(world_id); - e.set_position([4.0, 102.0, 0.0]); - - let (_, e) = server.entities.insert(EntityKind::GlowItemFrame, ()); - if let TrackedData::GlowItemFrame(i) = e.data_mut() { - i.set_rotation(Facing::West as i32); - } - e.set_world(world_id); - e.set_position([5.0, 102.0, 0.0]); - - // ==== Paintings ==== // - let (_, e) = server.entities.insert(EntityKind::Painting, ()); - if let TrackedData::Painting(p) = e.data_mut() { - p.set_variant(PaintingKind::Pigscene); - } - e.set_world(world_id); - e.set_yaw(180.0); - e.set_position([0.0, 102.0, 0.0]); - - let (_, e) = server.entities.insert(EntityKind::Painting, ()); - if let TrackedData::Painting(p) = e.data_mut() { - p.set_variant(PaintingKind::DonkeyKong); - } - e.set_world(world_id); - e.set_yaw(90.0); - e.set_position([-4.0, 102.0, 0.0]); - - let (_, e) = server.entities.insert(EntityKind::Painting, ()); - if let TrackedData::Painting(p) = e.data_mut() { - p.set_variant(PaintingKind::Void); - } - e.set_world(world_id); - e.set_position([-6.0, 102.0, 0.0]); - - let (_, e) = server.entities.insert(EntityKind::Painting, ()); - if let TrackedData::Painting(p) = e.data_mut() { - p.set_variant(PaintingKind::Aztec); - } - e.set_yaw(270.0); - e.set_world(world_id); - e.set_position([-7.0, 102.0, 0.0]); - - // ==== Shulkers ==== // - let (_, e) = server.entities.insert(EntityKind::Shulker, ()); - if let TrackedData::Shulker(s) = e.data_mut() { - s.set_peek_amount(100); - s.set_attached_face(Facing::West); - } - e.set_world(world_id); - e.set_position([-4.0, 102.0, -8.0]); - - let (_, e) = server.entities.insert(EntityKind::Shulker, ()); - if let TrackedData::Shulker(s) = e.data_mut() { - s.set_peek_amount(75); - s.set_attached_face(Facing::Up); - } - e.set_world(world_id); - e.set_position([-1.0, 102.0, -8.0]); - - let (_, e) = server.entities.insert(EntityKind::Shulker, ()); - if let TrackedData::Shulker(s) = e.data_mut() { - s.set_peek_amount(50); - s.set_attached_face(Facing::Down); - } - e.set_world(world_id); - e.set_position([2.0, 102.0, -8.0]); - - let (_, e) = server.entities.insert(EntityKind::Shulker, ()); - if let TrackedData::Shulker(s) = e.data_mut() { - s.set_peek_amount(25); - s.set_attached_face(Facing::East); - } - e.set_world(world_id); - e.set_position([5.0, 102.0, -8.0]); - - let (_, e) = server.entities.insert(EntityKind::Shulker, ()); - if let TrackedData::Shulker(s) = e.data_mut() { - s.set_peek_amount(0); - s.set_attached_face(Facing::North); - } - e.set_world(world_id); - e.set_position([8.0, 102.0, -8.0]); - - // ==== Slimes ==== // - let (_, e) = server.entities.insert(EntityKind::Slime, ()); - if let TrackedData::Slime(s) = e.data_mut() { - s.set_slime_size(30); - } - e.set_world(world_id); - e.set_yaw(180.0); - e.set_head_yaw(180.0); - e.set_position([12.0, 102.0, 10.0]); - - let (_, e) = server.entities.insert(EntityKind::MagmaCube, ()); - if let TrackedData::MagmaCube(m) = e.data_mut() { - m.set_slime_size(30); - } - e.set_world(world_id); - e.set_yaw(180.0); - e.set_head_yaw(180.0); - e.set_position([-12.0, 102.0, 10.0]); - - // ==== Sheep ==== // - let (_, e) = server.entities.insert(EntityKind::Sheep, ()); - if let TrackedData::Sheep(s) = e.data_mut() { - s.set_color(6); - s.set_child(true); - } - e.set_world(world_id); - e.set_position([-5.0, 101.0, -4.5]); - e.set_yaw(270.0); - e.set_head_yaw(270.0); - - let (_, e) = server.entities.insert(EntityKind::Sheep, ()); - if let TrackedData::Sheep(s) = e.data_mut() { - s.set_color(6); - } - e.set_world(world_id); - e.set_position([5.0, 101.0, -4.5]); - e.set_yaw(90.0); - e.set_head_yaw(90.0); - - // ==== Players ==== // - let player_poses = [ - Pose::Standing, - Pose::Sneaking, - Pose::FallFlying, - Pose::Sleeping, - Pose::Swimming, - Pose::SpinAttack, - Pose::Dying, - ]; - - for (i, pose) in player_poses.into_iter().enumerate() { - player_list.insert( - Uuid::from_u128(i as u128), - format!("fake_player_{i}"), - None, - GameMode::Survival, - 0, - None, - true, - ); - - let (_, e) = server - .entities - .insert_with_uuid(EntityKind::Player, Uuid::from_u128(i as u128), ()) - .unwrap(); - if let TrackedData::Player(p) = e.data_mut() { - p.set_pose(pose); - } - e.set_world(world_id); - e.set_position([-3.0 + i as f64 * 2.0, 104.0, -9.0]); - } - - // ==== Warden ==== // - let (_, e) = server.entities.insert(EntityKind::Warden, ()); - e.set_world(world_id); - e.set_position([-7.0, 102.0, -4.5]); - e.set_yaw(270.0); - e.set_head_yaw(270.0); - - let (_, e) = server.entities.insert(EntityKind::Warden, ()); - if let TrackedData::Warden(w) = e.data_mut() { - w.set_pose(Pose::Emerging); - } - e.set_world(world_id); - e.set_position([-7.0, 102.0, -6.5]); - e.set_yaw(270.0); - e.set_head_yaw(270.0); - - // ==== Goat ==== // - let (_, e) = server.entities.insert(EntityKind::Goat, ()); - e.set_world(world_id); - e.set_position([5.0, 103.0, -4.5]); - e.set_yaw(270.0); - e.set_head_yaw(90.0); - - let (_, e) = server.entities.insert(EntityKind::Goat, ()); - if let TrackedData::Goat(g) = e.data_mut() { - g.set_pose(Pose::LongJumping); - } - e.set_world(world_id); - e.set_position([5.0, 103.0, -3.5]); - e.set_yaw(270.0); - e.set_head_yaw(90.0); - - // ==== Giant ==== // - let (_, e) = server.entities.insert(EntityKind::Giant, ()); - e.set_world(world_id); - e.set_position([20.0, 101.0, -5.0]); - e.set_yaw(270.0); - e.set_head_yaw(90.0); - } - - fn update(&self, server: &mut Server) { - let world_id = server.state.world; - - // Rebuild our BVH every tick. All of the entities are in the same world. - server.state.bvh.rebuild( - server - .entities - .iter() - .map(|(id, entity)| WithAabb::new(id, entity.hitbox())), - ); - - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } - - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, entity)) => { - entity.set_world(world_id); - client.player = id - } - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } - - client.respawn(world_id); - client.set_flat(true); - client.set_game_mode(GameMode::Creative); - client.teleport( - [ - SPAWN_POS.x as f64 + 0.5, - SPAWN_POS.y as f64 + 1.0, - SPAWN_POS.z as f64 + 0.5, - ], - 0.0, - 0.0, - ); - client.set_player_list(server.state.player_list.clone()); - - if let Some(id) = &server.state.player_list { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - - client.send_message( - "Press ".italic() - + "F3 + B".italic().color(Color::AQUA) - + " to show hitboxes.".italic(), - ); - } - - let player = &mut server.entities[client.player]; - - while let Some(event) = client.next_event() { - event.handle_default(client, player); - } - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - if let Some(id) = &server.state.player_list { - server.player_lists[id].remove(client.uuid()); - } - player.set_deleted(true); - - return false; - } - - let client_pos = client.position(); - - let origin = Vec3::new(client_pos.x, client_pos.y + PLAYER_EYE_HEIGHT, client_pos.z); - let direction = from_yaw_and_pitch(client.yaw() as f64, client.pitch() as f64); - let not_self_or_bullet = |hit: RaycastHit>| { - hit.object.object != client.player && hit.object.object != client.shulker_bullet - }; - - if let Some(hit) = server - .state - .bvh - .raycast(origin, direction, not_self_or_bullet) - { - let bullet = if let Some(bullet) = server.entities.get_mut(client.shulker_bullet) { - bullet - } else { - let (id, bullet) = server.entities.insert(EntityKind::ShulkerBullet, ()); - client.shulker_bullet = id; - bullet.set_world(world_id); - bullet - }; - - let mut hit_pos = origin + direction * hit.near; - let hitbox = bullet.hitbox(); - - hit_pos.y -= (hitbox.max.y - hitbox.min.y) / 2.0; - - bullet.set_position(hit_pos); - - client.set_action_bar("Intersection".color(Color::GREEN)); - } else { - server.entities.delete(client.shulker_bullet); - client.set_action_bar("No Intersection".color(Color::RED)); - } - - true - }); - } -} diff --git a/crates/valence/examples/gamemode_switcher.rs b/crates/valence/examples/gamemode_switcher.rs new file mode 100644 index 0000000..cca1aff --- /dev/null +++ b/crates/valence/examples/gamemode_switcher.rs @@ -0,0 +1,82 @@ +use valence::client::despawn_disconnected_clients; +use valence::client::event::{default_event_handler, ChatCommand}; +use valence::prelude::*; + +const SPAWN_Y: i32 = 64; + +pub fn main() { + tracing_subscriber::fmt().init(); + + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_to_stage(EventLoop, interpret_command) + .add_system_set(PlayerList::default_system_set()) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .run(); +} + +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); + + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); + } + } + + for z in -25..25 { + for x in -25..25 { + instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + world.spawn(instance); +} + +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + for mut client in &mut clients { + client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + client.set_instance(instances.single()); + client.set_game_mode(GameMode::Creative); + client.set_op_level(2); // required to use F3+F4, eg /gamemode + client.send_message("Welcome to Valence! Use F3+F4 to change gamemode.".italic()); + } +} + +fn interpret_command(mut clients: Query<&mut Client>, mut events: EventReader) { + for event in events.iter() { + let Ok(mut client) = clients.get_component_mut::(event.client) else { + continue; + }; + + let mut args = event.command.split_whitespace(); + + if args.next() == Some("gamemode") { + if client.op_level() < 2 { + // not enough permissions to use gamemode command + continue; + } + + let mode = match args.next().unwrap_or_default() { + "adventure" => GameMode::Adventure, + "creative" => GameMode::Creative, + "survival" => GameMode::Survival, + "spectator" => GameMode::Spectator, + _ => { + client.send_message("Invalid gamemode.".italic()); + continue; + } + }; + client.set_game_mode(mode); + client.send_message(format!("Set gamemode to {mode:?}.").italic()); + } + } +} diff --git a/crates/valence/examples/inventory_piano.rs b/crates/valence/examples/inventory_piano.rs deleted file mode 100644 index f8b6865..0000000 --- a/crates/valence/examples/inventory_piano.rs +++ /dev/null @@ -1,236 +0,0 @@ -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use num::Integer; -pub use valence::prelude::*; -use valence_protocol::types::ClickContainerMode; - -pub fn main() -> ShutdownResult { - tracing_subscriber::fmt().init(); - - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - ServerState { player_list: None }, - ) -} - -struct Game { - player_count: AtomicUsize, -} - -struct ServerState { - player_list: Option, -} - -#[derive(Default)] -struct ClientState { - entity_id: EntityId, -} - -const MAX_PLAYERS: usize = 10; - -const SIZE_X: usize = 100; -const SIZE_Z: usize = 100; - -const SLOT_MIN: i16 = 36; -const SLOT_MAX: i16 = 43; -const PITCH_MIN: f32 = 0.5; -const PITCH_MAX: f32 = 1.0; - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - fn dimensions(&self) -> Vec { - vec![Dimension { - fixed_time: Some(6000), - ..Dimension::default() - }] - } - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), - } - } - - fn init(&self, server: &mut Server) { - let world = server.worlds.insert(DimensionId::default(), ()).1; - server.state.player_list = Some(server.player_lists.insert(()).0); - - // initialize chunks - for chunk_z in -2..Integer::div_ceil(&(SIZE_Z as i32), &16) + 2 { - for chunk_x in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 { - world - .chunks - .insert([chunk_x, chunk_z], UnloadedChunk::default(), ()); - } - } - - // initialize blocks in the chunks - for chunk_x in 0..Integer::div_ceil(&SIZE_X, &16) { - for chunk_z in 0..Integer::div_ceil(&SIZE_Z, &16) { - let chunk = world - .chunks - .get_mut([chunk_x as i32, chunk_z as i32]) - .unwrap(); - for x in 0..16 { - for z in 0..16 { - let cell_x = chunk_x * 16 + x; - let cell_z = chunk_z * 16 + z; - - if cell_x < SIZE_X && cell_z < SIZE_Z { - chunk.set_block_state(x, 63, z, BlockState::GRASS_BLOCK); - } - } - } - } - } - } - - fn update(&self, server: &mut Server) { - let (world_id, _) = server.worlds.iter_mut().next().unwrap(); - - let spawn_pos = [SIZE_X as f64 / 2.0, 1.0, SIZE_Z as f64 / 2.0]; - - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } - - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, _)) => client.state.entity_id = id, - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } - - client.respawn(world_id); - client.set_flat(true); - client.teleport(spawn_pos, 0.0, 0.0); - client.set_player_list(server.state.player_list.clone()); - - if let Some(id) = &server.state.player_list { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - - client.set_game_mode(GameMode::Creative); - client.send_message( - "Welcome to Valence! Open your inventory, and click on your hotbar to play \ - the piano." - .italic(), - ); - client.send_message( - "Click the rightmost hotbar slot to toggle between creative and survival." - .italic(), - ); - } - - let player = server.entities.get_mut(client.state.entity_id).unwrap(); - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - player.set_deleted(true); - if let Some(id) = &server.state.player_list { - server.player_lists[id].remove(client.uuid()); - } - return false; - } - - if client.position().y <= -20.0 { - client.teleport(spawn_pos, client.yaw(), client.pitch()); - } - - while let Some(event) = client.next_event() { - match event { - ClientEvent::CloseContainer { .. } => { - client.send_message("Done already?"); - } - ClientEvent::SetCreativeModeSlot { slot, .. } => { - client.send_message(format!("{event:#?}")); - // If the user does a double click, 3 notes will be played. - // This is not possible to fix :( - play_note(client, player, slot); - } - ClientEvent::ClickContainer { slot_id, mode, .. } => { - client.send_message(format!("{event:#?}")); - if mode != ClickContainerMode::Click { - // Prevent notes from being played twice if the user clicks quickly - continue; - } - play_note(client, player, slot_id); - } - _ => {} - } - } - - true - }); - } -} - -fn play_note(client: &mut Client, player: &mut Entity, clicked_slot: i16) { - if (SLOT_MIN..=SLOT_MAX).contains(&clicked_slot) { - let pitch = (clicked_slot - SLOT_MIN) as f32 * (PITCH_MAX - PITCH_MIN) - / (SLOT_MAX - SLOT_MIN) as f32 - + PITCH_MIN; - - client.send_message(format!("playing note with pitch: {pitch}")); - - let _ = player; - // client.play_sound( - // Ident::new("block.note_block.harp").unwrap(), - // SoundCategory::Block, - // player.position(), - // 10.0, - // pitch, - // ); - } else if clicked_slot == 44 { - client.set_game_mode(match client.game_mode() { - GameMode::Survival => GameMode::Creative, - GameMode::Creative => GameMode::Survival, - _ => GameMode::Creative, - }); - } -} diff --git a/crates/valence/examples/parkour.rs b/crates/valence/examples/parkour.rs index fc58c7b..cc0cc50 100644 --- a/crates/valence/examples/parkour.rs +++ b/crates/valence/examples/parkour.rs @@ -1,45 +1,15 @@ use std::collections::VecDeque; -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use rand::seq::SliceRandom; use rand::Rng; +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; use valence::prelude::*; +use valence_protocol::packets::s2c::play::SetTitleAnimationTimes; -pub fn main() -> ShutdownResult { - tracing_subscriber::fmt().init(); - - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - None, - ) -} - -struct Game { - player_count: AtomicUsize, -} - -#[derive(Default)] -struct ChunkState { - keep_loaded: bool, -} - -#[derive(Default)] -struct ClientState { - entity_id: EntityId, - blocks: VecDeque, - score: u32, - combo: u32, - target_y: i32, - last_block_timestamp: u128, - world_id: WorldId, -} - -const MAX_PLAYERS: usize = 10; const START_POS: BlockPos = BlockPos::new(0, 100, 0); +const VIEW_DIST: u8 = 10; const BLOCK_TYPES: [BlockState; 7] = [ BlockState::GRASS_BLOCK, @@ -51,273 +21,217 @@ const BLOCK_TYPES: [BlockState; 7] = [ BlockState::MOSS_BLOCK, ]; -#[async_trait] -impl Config for Game { - type ServerState = Option; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = ChunkState; - type PlayerListState = (); - type InventoryState = (); +pub fn main() { + tracing_subscriber::fmt().init(); - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), - } - } - - fn init(&self, server: &mut Server) { - server.state = Some(server.player_lists.insert(()).0); - } - - fn update(&self, server: &mut Server) { - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } - - let (world_id, world) = server.worlds.insert(DimensionId::default(), ()); - - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, entity)) => { - entity.set_world(world_id); - - // create client state - client.state = ClientState { - entity_id: id, - blocks: VecDeque::new(), - score: 0, - combo: 0, - last_block_timestamp: 0, - target_y: 0, - world_id, - }; - } - None => { - client.disconnect("Conflicting UUID"); - server.worlds.remove(world_id); - return false; - } - } - - if let Some(id) = &server.state { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - - client.respawn(world_id); - client.set_flat(true); - client.set_player_list(server.state.clone()); - - client.send_message("Welcome to epic infinite parkour game!".italic()); - client.set_game_mode(GameMode::Adventure); - reset(client, world); - } - - let world_id = client.world_id; - let world = &mut server.worlds[world_id]; - - let p = client.position(); - for pos in ChunkPos::at(p.x, p.z).in_view(3) { - if let Some(chunk) = world.chunks.get_mut(pos) { - chunk.keep_loaded = true; - } else { - world.chunks.insert( - pos, - UnloadedChunk::default(), - ChunkState { keep_loaded: true }, - ); - } - } - - if (client.position().y as i32) < START_POS.y - 32 { - client.send_message( - "Your score was ".italic() - + client - .score - .to_string() - .color(Color::GOLD) - .bold() - .not_italic(), - ); - - reset(client, world); - } - - let pos_under_player = BlockPos::new( - (client.position().x - 0.5).round() as i32, - client.position().y as i32 - 1, - (client.position().z - 0.5).round() as i32, - ); - - if let Some(index) = client - .blocks - .iter() - .position(|block| *block == pos_under_player) - { - if index > 0 { - let power_result = 2.0f32.powf((client.combo as f32) / 45.0); - let max_time_taken = (1000.0f32 * (index as f32) / power_result) as u128; - - let current_time_millis = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); - if current_time_millis - client.last_block_timestamp < max_time_taken { - client.combo += index as u32 - } else { - client.combo = 0 - } - - // let pitch = 0.9 + ((client.combo as f32) - 1.0) * 0.05; - - for _ in 0..index { - generate_next_block(client, world, true) - } - - // TODO: add sounds again. - // client.play_sound( - // Ident::new("minecraft:block.note_block.bass").unwrap(), - // SoundCategory::Master, - // client.position(), - // 1f32, - // pitch, - // ); - - client.set_title( - "", - client.score.to_string().color(Color::LIGHT_PURPLE).bold(), - SetTitleAnimationTimes { - fade_in: 0, - stay: 7, - fade_out: 4, - }, - ); - } - } - - let player = server.entities.get_mut(client.entity_id).unwrap(); - - while let Some(event) = client.next_event() { - event.handle_default(client, player); - } - - // Remove chunks outside the view distance of players. - for (_, chunk) in world.chunks.iter_mut() { - chunk.set_deleted(!chunk.keep_loaded); - chunk.keep_loaded = false; - } - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - if let Some(id) = &server.state { - server.player_lists[id].remove(client.uuid()); - } - player.set_deleted(true); - server.worlds.remove(world_id); - return false; - } - - true - }); - } + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_set(PlayerList::default_system_set()) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .add_system(reset_clients.after(init_clients)) + .add_system(manage_chunks.after(reset_clients).before(manage_blocks)) + .add_system(manage_blocks) + .run(); } -fn reset(client: &mut Client, world: &mut World) { - // Load chunks around spawn to avoid double void reset - for chunk_z in -1..3 { - for chunk_x in -2..2 { - world.chunks.insert( - [chunk_x, chunk_z], - UnloadedChunk::default(), - ChunkState { keep_loaded: true }, - ); +#[derive(Component)] +struct GameState { + blocks: VecDeque, + score: u32, + combo: u32, + target_y: i32, + last_block_timestamp: u128, +} + +fn init_clients( + mut commands: Commands, + server: Res, + mut clients: Query<(Entity, &mut Client), Added>, +) { + for (ent, mut client) in clients.iter_mut() { + let mut instance = server.new_instance(DimensionId::default()); + + for pos in client.view().with_dist(VIEW_DIST).iter() { + assert!(instance.insert_chunk(pos, Chunk::default()).is_none()); } - } - client.score = 0; - client.combo = 0; - - for block in &client.blocks { - world.chunks.set_block_state(*block, BlockState::AIR); - } - client.blocks.clear(); - client.blocks.push_back(START_POS); - world.chunks.set_block_state(START_POS, BlockState::STONE); - - for _ in 0..10 { - generate_next_block(client, world, false) - } - - client.teleport( - [ + client.set_position([ START_POS.x as f64 + 0.5, START_POS.y as f64 + 1.0, START_POS.z as f64 + 0.5, - ], - 0f32, - 0f32, - ); + ]); + client.set_flat(true); + client.set_instance(ent); + client.set_game_mode(GameMode::Adventure); + client.send_message("Welcome to epic infinite parkour game!".italic()); + + let mut state = GameState { + blocks: VecDeque::new(), + score: 0, + combo: 0, + target_y: 0, + last_block_timestamp: 0, + }; + + reset(&mut client, &mut state, &mut instance); + + commands.entity(ent).insert(state); + commands.entity(ent).insert(instance); + } } -fn generate_next_block(client: &mut Client, world: &mut World, in_game: bool) { - if in_game { - let removed_block = client.blocks.pop_front().unwrap(); - world.chunks.set_block_state(removed_block, BlockState::AIR); +fn reset_clients( + mut clients: Query<(&mut Client, &mut GameState, &mut Instance), With>, +) { + for (mut client, mut state, mut instance) in clients.iter_mut() { + if (client.position().y as i32) < START_POS.y - 32 { + client.send_message( + "Your score was ".italic() + + state + .score + .to_string() + .color(Color::GOLD) + .bold() + .not_italic(), + ); - client.score += 1 + reset(&mut client, &mut state, &mut instance); + } + } +} + +fn manage_blocks(mut clients: Query<(&mut Client, &mut GameState, &mut Instance)>) { + for (mut client, mut state, mut instance) in clients.iter_mut() { + let pos_under_player = BlockPos::new( + (client.position().x - 0.5).round() as i32, + client.position().y as i32 - 1, + (client.position().z - 0.5).round() as i32, + ); + + if let Some(index) = state + .blocks + .iter() + .position(|block| *block == pos_under_player) + { + if index > 0 { + let power_result = 2.0f32.powf((state.combo as f32) / 45.0); + let max_time_taken = (1000.0f32 * (index as f32) / power_result) as u128; + + let current_time_millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); + + if current_time_millis - state.last_block_timestamp < max_time_taken { + state.combo += index as u32 + } else { + state.combo = 0 + } + + // let pitch = 0.9 + ((state.combo as f32) - 1.0) * 0.05; + + for _ in 0..index { + generate_next_block(&mut state, &mut instance, true) + } + + // TODO: add sounds again. + // client.play_sound( + // Ident::new("minecraft:block.note_block.bass").unwrap(), + // SoundCategory::Master, + // client.position(), + // 1f32, + // pitch, + // ); + + client.set_title( + "", + state.score.to_string().color(Color::LIGHT_PURPLE).bold(), + SetTitleAnimationTimes { + fade_in: 0, + stay: 7, + fade_out: 4, + }, + ); + } + } + } +} + +fn manage_chunks(mut clients: Query<(&mut Client, &mut Instance)>) { + for (client, mut instance) in &mut clients { + let old_view = client.old_view().with_dist(VIEW_DIST); + let view = client.view().with_dist(VIEW_DIST); + + if old_view != view { + for pos in old_view.diff(view) { + instance.chunk_entry(pos).or_default(); + } + + for pos in view.diff(old_view) { + instance.chunk_entry(pos).or_default(); + } + } + } +} + +fn reset(client: &mut Client, state: &mut GameState, instance: &mut Instance) { + // Load chunks around spawn to avoid double void reset + for z in -1..3 { + for x in -2..2 { + instance.insert_chunk([x, z], Chunk::default()); + } } - let last_pos = *client.blocks.back().unwrap(); - let block_pos = generate_random_block(last_pos, client.target_y); + state.score = 0; + state.combo = 0; + + for block in &state.blocks { + instance.set_block_state(*block, BlockState::AIR); + } + state.blocks.clear(); + state.blocks.push_back(START_POS); + instance.set_block_state(START_POS, BlockState::STONE); + + for _ in 0..10 { + generate_next_block(state, instance, false); + } + + client.set_position([ + START_POS.x as f64 + 0.5, + START_POS.y as f64 + 1.0, + START_POS.z as f64 + 0.5, + ]); + client.set_velocity([0f32, 0f32, 0f32]); + client.set_yaw(0f32); + client.set_pitch(0f32) +} + +fn generate_next_block(state: &mut GameState, instance: &mut Instance, in_game: bool) { + if in_game { + let removed_block = state.blocks.pop_front().unwrap(); + instance.set_block_state(removed_block, BlockState::AIR); + + state.score += 1 + } + + let last_pos = *state.blocks.back().unwrap(); + let block_pos = generate_random_block(last_pos, state.target_y); if last_pos.y == START_POS.y { - client.target_y = 0 + state.target_y = 0 } else if last_pos.y < START_POS.y - 30 || last_pos.y > START_POS.y + 30 { - client.target_y = START_POS.y; + state.target_y = START_POS.y; } let mut rng = rand::thread_rng(); - world - .chunks - .set_block_state(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap()); - client.blocks.push_back(block_pos); + instance.set_block_state(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap()); + state.blocks.push_back(block_pos); // Combo System - client.last_block_timestamp = SystemTime::now() + state.last_block_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(); @@ -333,9 +247,9 @@ fn generate_random_block(pos: BlockPos, target_y: i32) -> BlockPos { _ => -1, }; let z = match y { - 1 => rng.gen_range(1..4), - -1 => rng.gen_range(2..6), - _ => rng.gen_range(1..5), + 1 => rng.gen_range(1..3), + -1 => rng.gen_range(2..5), + _ => rng.gen_range(1..4), }; let x = rng.gen_range(-3..4); diff --git a/crates/valence/examples/particles.rs b/crates/valence/examples/particles.rs index 1100841..6245349 100644 --- a/crates/valence/examples/particles.rs +++ b/crates/valence/examples/particles.rs @@ -1,199 +1,99 @@ -use std::fmt; -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; - +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; use valence::prelude::*; +use valence_protocol::packets::s2c::particle::Particle; -pub fn main() -> ShutdownResult { +const SPAWN_Y: i32 = 64; + +pub fn main() { tracing_subscriber::fmt().init(); - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - ServerState { - player_list: None, - particle_list: create_particle_vec(), - particle_idx: 0, - }, - ) + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_set(PlayerList::default_system_set()) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .add_system(manage_particles) + .run(); } -struct Game { - player_count: AtomicUsize, +#[derive(Resource)] +struct ParticleSpawner { + particles: Vec, + index: usize, } -struct ServerState { - player_list: Option, - particle_list: Vec, - particle_idx: usize, -} - -const MAX_PLAYERS: usize = 10; - -const SPAWN_POS: BlockPos = BlockPos::new(0, 100, 0); - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = EntityId; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), +impl ParticleSpawner { + pub fn new() -> Self { + Self { + particles: create_particle_vec(), + index: 0, } } - fn init(&self, server: &mut Server) { - let (_, world) = server.worlds.insert(DimensionId::default(), ()); - server.state.player_list = Some(server.player_lists.insert(()).0); - - let size = 5; - for z in -size..size { - for x in -size..size { - world.chunks.insert([x, z], UnloadedChunk::default(), ()); - } - } - - world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK); - } - - fn update(&self, server: &mut Server) { - let (world_id, _) = server.worlds.iter_mut().next().expect("missing world"); - - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } - - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, _)) => client.state = id, - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } - - client.respawn(world_id); - client.set_flat(true); - client.set_game_mode(GameMode::Creative); - client.teleport( - [ - SPAWN_POS.x as f64 + 0.5, - SPAWN_POS.y as f64 + 1.0, - SPAWN_POS.z as f64 + 0.5, - ], - 0.0, - 0.0, - ); - client.set_player_list(server.state.player_list.clone()); - client.send_message("Sneak to speed up the cycling of particles"); - - if let Some(id) = &server.state.player_list { - server.player_lists.get_mut(id).insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - } - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - if let Some(id) = &server.state.player_list { - server.player_lists[id].remove(client.uuid()); - } - server.entities[client.state].set_deleted(true); - - return false; - } - - let entity = server - .entities - .get_mut(client.state) - .expect("missing player entity"); - - while let Some(event) = client.next_event() { - event.handle_default(client, entity); - } - - true - }); - - let players_are_sneaking = server.clients.iter().any(|(_, client)| -> bool { - let player = &server.entities[client.state]; - if let TrackedData::Player(data) = player.data() { - return data.get_pose() == Pose::Sneaking; - } - false - }); - - let cycle_time = if players_are_sneaking { 5 } else { 30 }; - - if !server.clients.is_empty() && server.current_tick() % cycle_time == 0 { - if server.state.particle_idx == server.state.particle_list.len() { - server.state.particle_idx = 0; - } - - let pos = [ - SPAWN_POS.x as f64 + 0.5, - SPAWN_POS.y as f64 + 2.0, - SPAWN_POS.z as f64 + 5.5, - ]; - let offset = [0.5, 0.5, 0.5]; - let particle = &server.state.particle_list[server.state.particle_idx]; - - server.clients.iter_mut().for_each(|(_, client)| { - client.set_title( - "", - dbg_name(particle).bold(), - SetTitleAnimationTimes { - fade_in: 0, - stay: 100, - fade_out: 2, - }, - ); - client.play_particle(particle, true, pos, offset, 0.1, 100); - }); - server.state.particle_idx += 1; - } + pub fn next(&mut self) { + self.index = (self.index + 1) % self.particles.len(); } } -fn dbg_name(dbg: &impl fmt::Debug) -> String { +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); + + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); + } + } + + instance.set_block_state([0, SPAWN_Y, 0], BlockState::BEDROCK); + + world.spawn(instance); + + let spawner = ParticleSpawner::new(); + world.insert_resource(spawner) +} + +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + for mut client in &mut clients { + client.set_position([0.5, SPAWN_Y as f64 + 1.0, 0.5]); + client.set_instance(instances.single()); + client.set_game_mode(GameMode::Creative); + } +} + +fn manage_particles( + mut spawner: ResMut, + server: Res, + mut instances: Query<&mut Instance>, +) { + if server.current_tick() % 20 == 0 { + spawner.next(); + } + + if server.current_tick() % 5 != 0 { + return; + } + + let particle = &spawner.particles[spawner.index]; + let name = dbg_name(particle); + + let pos = [0.5, SPAWN_Y as f64 + 2.0, 5.0]; + let offset = [0.5, 0.5, 0.5]; + + let mut instance = instances.single_mut(); + + instance.play_particle(particle, true, pos, offset, 0.1, 100); + instance.set_action_bar(name.bold()); +} + +fn dbg_name(dbg: &impl std::fmt::Debug) -> String { let string = format!("{dbg:?}"); string @@ -253,7 +153,7 @@ fn create_particle_vec() -> Vec { Particle::Item(None), Particle::Item(Some(ItemStack::new(ItemKind::IronPickaxe, 1, None))), Particle::VibrationBlock { - block_pos: SPAWN_POS, + block_pos: [0, SPAWN_Y, 0].into(), ticks: 50, }, Particle::VibrationEntity { diff --git a/crates/valence/examples/player_list.rs b/crates/valence/examples/player_list.rs new file mode 100644 index 0000000..9dccb5f --- /dev/null +++ b/crates/valence/examples/player_list.rs @@ -0,0 +1,120 @@ +use rand::Rng; +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; +use valence::player_list::Entry; +use valence::prelude::*; + +const SPAWN_Y: i32 = 64; +const PLAYER_UUID_1: Uuid = Uuid::from_u128(1); +const PLAYER_UUID_2: Uuid = Uuid::from_u128(2); + +fn main() { + tracing_subscriber::fmt().init(); + + App::new() + .add_plugin(ServerPlugin::new(())) + .add_startup_system(setup) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system(init_clients) + .add_system(update_player_list) + .add_system(despawn_disconnected_clients) + .add_system(remove_disconnected_clients_from_player_list) + .run(); +} + +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); + + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); + } + } + + for z in -25..25 { + for x in -25..25 { + instance.set_block_state([x, SPAWN_Y, z], BlockState::LIGHT_GRAY_WOOL); + } + } + + world.spawn(instance); + + let mut player_list = world.resource_mut::(); + + player_list.insert( + PLAYER_UUID_1, + PlayerListEntry::new().with_display_name(Some("persistent entry with no ping")), + ); +} + +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, + mut player_list: ResMut, +) { + for mut client in &mut clients { + client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + client.set_instance(instances.single()); + client.set_game_mode(GameMode::Creative); + + client.send_message( + "Please open your player list (tab key)." + .italic() + .color(Color::WHITE), + ); + + let entry = PlayerListEntry::new() + .with_username(client.username()) + .with_properties(client.properties()) // For the player's skin and cape. + .with_game_mode(client.game_mode()) + .with_ping(0) // Use negative values to indicate missing. + .with_display_name(Some("ඞ".color(Color::new(255, 87, 66)))); + + player_list.insert(client.uuid(), entry); + } +} + +fn update_player_list(mut player_list: ResMut, server: Res) { + let tick = server.current_tick(); + + player_list.set_header("Current tick: ".into_text() + tick); + player_list + .set_footer("Current tick but in purple: ".into_text() + tick.color(Color::LIGHT_PURPLE)); + + if tick % 5 == 0 { + let mut rng = rand::thread_rng(); + let color = Color::new(rng.gen(), rng.gen(), rng.gen()); + + let entry = player_list.get_mut(PLAYER_UUID_1).unwrap(); + let new_display_name = entry.display_name().unwrap().clone().color(color); + entry.set_display_name(Some(new_display_name)); + } + + if tick % 20 == 0 { + match player_list.entry(PLAYER_UUID_2) { + Entry::Occupied(oe) => { + oe.remove(); + } + Entry::Vacant(ve) => { + let entry = PlayerListEntry::new() + .with_display_name(Some("Hello!")) + .with_ping(300); + + ve.insert(entry); + } + } + } +} + +fn remove_disconnected_clients_from_player_list( + clients: Query<&mut Client>, + mut player_list: ResMut, +) { + for client in &clients { + if client.is_disconnected() { + player_list.remove(client.uuid()); + } + } +} diff --git a/crates/valence/examples/resource_pack.rs b/crates/valence/examples/resource_pack.rs index 1c3f51c..f2c3579 100644 --- a/crates/valence/examples/resource_pack.rs +++ b/crates/valence/examples/resource_pack.rs @@ -1,207 +1,101 @@ -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; - +use valence::client::despawn_disconnected_clients; +use valence::client::event::{ + default_event_handler, InteractWithEntity, ResourcePackStatus, ResourcePackStatusChange, +}; use valence::prelude::*; use valence_protocol::types::EntityInteraction; -pub fn main() -> ShutdownResult { +const SPAWN_Y: i32 = 64; + +pub fn main() { tracing_subscriber::fmt().init(); - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - }, - ServerState { - player_list: None, - sheep_id: EntityId::NULL, - }, - ) + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_to_stage(EventLoop, prompt_on_punch) + .add_system_to_stage(EventLoop, on_resource_pack_status) + .add_system_set(PlayerList::default_system_set()) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .run(); } -struct Game { - player_count: AtomicUsize, -} +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); -struct ServerState { - player_list: Option, - sheep_id: EntityId, -} - -#[derive(Default)] -struct ClientState { - entity_id: EntityId, -} - -const MAX_PLAYERS: usize = 10; - -const SPAWN_POS: BlockPos = BlockPos::new(0, 100, 0); - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); } } - fn init(&self, server: &mut Server) { - let (world_id, world) = server.worlds.insert(DimensionId::default(), ()); - server.state.player_list = Some(server.player_lists.insert(()).0); - - let size = 5; - for z in -size..size { - for x in -size..size { - world.chunks.insert([x, z], UnloadedChunk::default(), ()); - } + for z in -25..25 { + for x in -25..25 { + instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } - - let (sheep_id, sheep) = server.entities.insert(EntityKind::Sheep, ()); - server.state.sheep_id = sheep_id; - sheep.set_world(world_id); - sheep.set_position([ - SPAWN_POS.x as f64 + 0.5, - SPAWN_POS.y as f64 + 4.0, - SPAWN_POS.z as f64 + 0.5, - ]); - - if let TrackedData::Sheep(sheep_data) = sheep.data_mut() { - sheep_data.set_custom_name("Hit me".color(Color::GREEN)); - } - - world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK); } - fn update(&self, server: &mut Server) { - let (world_id, _) = server.worlds.iter_mut().next().expect("missing world"); + let instance_ent = world.spawn(instance).id(); - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } + let mut sheep = McEntity::new(EntityKind::Sheep, instance_ent); + sheep.set_position([0.0, SPAWN_Y as f64 + 1.0, 2.0]); + world.spawn(sheep); +} - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, _)) => client.entity_id = id, - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + for mut client in &mut clients { + client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + client.set_instance(instances.single()); + client.set_game_mode(GameMode::Creative); - client.respawn(world_id); - client.set_flat(true); - client.set_game_mode(GameMode::Creative); - client.teleport( - [ - SPAWN_POS.x as f64 + 0.5, - SPAWN_POS.y as f64 + 1.0, - SPAWN_POS.z as f64 + 0.5, - ], - 0.0, - 0.0, - ); - client.set_player_list(server.state.player_list.clone()); - - if let Some(id) = &server.state.player_list { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - - client.send_message( - "Hit the sheep above you to prompt for the resource pack again.".italic(), - ); - - set_example_pack(client); - } - - let player = &mut server.entities[client.entity_id]; - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - if let Some(id) = &server.state.player_list { - server.player_lists[id].remove(client.uuid()); - } - player.set_deleted(true); - - return false; - } - - while let Some(event) = client.next_event() { - event.handle_default(client, player); - match event { - ClientEvent::InteractWithEntity { - entity_id, - interact, - .. - } => { - if interact == EntityInteraction::Attack - && entity_id == server.state.sheep_id.to_raw() - { - set_example_pack(client); - } - } - ClientEvent::ResourcePackLoaded => { - client.send_message("Resource pack loaded!".color(Color::GREEN)); - } - ClientEvent::ResourcePackDeclined => { - client.send_message("Resource pack declined.".color(Color::RED)); - } - ClientEvent::ResourcePackFailedDownload => { - client.send_message("Resource pack download failed.".color(Color::RED)); - } - _ => {} - } - } - - true - }); + client.send_message("Hit the sheep to prompt for the resource pack.".italic()); } } -/// Sends the resource pack prompt. -fn set_example_pack(client: &mut Client) { - client.set_resource_pack( - "https://github.com/valence-rs/valence/raw/main/assets/example_pack.zip", - "d7c6108849fb190ec2a49f2d38b7f1f897d9ce9f", - false, - None, - ); +fn prompt_on_punch(mut clients: Query<&mut Client>, mut events: EventReader) { + for event in events.iter() { + let Ok(mut client) = clients.get_mut(event.client) else { + continue; + }; + if event.interact == EntityInteraction::Attack { + client.set_resource_pack( + "https://github.com/valence-rs/valence/raw/main/assets/example_pack.zip", + "d7c6108849fb190ec2a49f2d38b7f1f897d9ce9f", + false, + None, + ); + } + } +} + +fn on_resource_pack_status( + mut clients: Query<&mut Client>, + mut events: EventReader, +) { + for event in events.iter() { + let Ok(mut client) = clients.get_mut(event.client) else { + continue; + }; + match event.status { + ResourcePackStatus::Accepted => { + client.send_message("Resource pack accepted.".color(Color::GREEN)); + } + ResourcePackStatus::Declined => { + client.send_message("Resource pack declined.".color(Color::RED)); + } + ResourcePackStatus::FailedDownload => { + client.send_message("Resource pack failed to download.".color(Color::RED)); + } + ResourcePackStatus::Loaded => { + client.send_message("Resource pack successfully downloaded.".color(Color::BLUE)); + } + } + } } diff --git a/crates/valence/examples/server_list_ping.rs b/crates/valence/examples/server_list_ping.rs new file mode 100644 index 0000000..36b42d6 --- /dev/null +++ b/crates/valence/examples/server_list_ping.rs @@ -0,0 +1,37 @@ +use std::net::SocketAddr; + +use valence::prelude::*; + +pub fn main() { + App::new() + .add_plugin(ServerPlugin::new(MyCallbacks).with_connection_mode(ConnectionMode::Offline)) + .run(); +} + +struct MyCallbacks; + +#[async_trait] +impl AsyncCallbacks for MyCallbacks { + async fn server_list_ping( + &self, + _shared: &SharedServer, + remote_addr: SocketAddr, + _protocol_version: i32, + ) -> ServerListPing { + ServerListPing::Respond { + online_players: 42, + max_players: 420, + player_sample: vec![PlayerSampleEntry { + name: "foobar".into(), + id: Uuid::from_u128(12345), + }], + description: "Your IP address is ".into_text() + + remote_addr.to_string().color(Color::GOLD), + favicon_png: include_bytes!("../../../assets/logo-64x64.png"), + } + } + + async fn login(&self, _shared: &SharedServer, _info: &NewClientInfo) -> Result<(), Text> { + Err("You are not meant to join this example".color(Color::RED)) + } +} diff --git a/crates/valence/examples/terrain.rs b/crates/valence/examples/terrain.rs index 346688b..564969e 100644 --- a/crates/valence/examples/terrain.rs +++ b/crates/valence/examples/terrain.rs @@ -1,313 +1,317 @@ -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::sync::Arc; +use std::thread; use std::time::SystemTime; +use flume::{Receiver, Sender}; use noise::{NoiseFn, SuperSimplex}; -use rayon::iter::ParallelIterator; -pub use valence::prelude::*; -use vek::Lerp; +use tracing::info; +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; +use valence::prelude::*; -pub fn main() -> ShutdownResult { +const SPAWN_POS: DVec3 = DVec3::new(0.0, 200.0, 0.0); +const SECTION_COUNT: usize = 24; + +struct ChunkWorkerState { + sender: Sender<(ChunkPos, Chunk)>, + receiver: Receiver, + // Noise functions + density: SuperSimplex, + hilly: SuperSimplex, + stone: SuperSimplex, + gravel: SuperSimplex, + grass: SuperSimplex, +} + +#[derive(Resource)] +struct GameState { + /// Chunks that need to be generated. Chunks without a priority have already + /// been sent to the thread pool. + pending: HashMap>, + sender: Sender, + receiver: Receiver<(ChunkPos, Chunk)>, +} + +/// The order in which chunks should be processed by the thread pool. Smaller +/// values are sent first. +type Priority = u64; + +pub fn main() { tracing_subscriber::fmt().init(); + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_set(PlayerList::default_system_set()) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(remove_unviewed_chunks.after(init_clients)) + .add_system(update_client_views.after(remove_unviewed_chunks)) + .add_system(send_recv_chunks.after(update_client_views)) + .add_system(despawn_disconnected_clients) + .run(); +} + +fn setup(world: &mut World) { let seconds_per_day = 86_400; let seed = (SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH)? + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() .as_secs() / seconds_per_day) as u32; - valence::start_server( - Game { - player_count: AtomicUsize::new(0), - density_noise: SuperSimplex::new(seed), - hilly_noise: SuperSimplex::new(seed.wrapping_add(1)), - stone_noise: SuperSimplex::new(seed.wrapping_add(2)), - gravel_noise: SuperSimplex::new(seed.wrapping_add(3)), - grass_noise: SuperSimplex::new(seed.wrapping_add(4)), - }, - None, - ) + info!("current seed: {seed}"); + + let (finished_sender, finished_receiver) = flume::unbounded(); + let (pending_sender, pending_receiver) = flume::unbounded(); + + let state = Arc::new(ChunkWorkerState { + sender: finished_sender, + receiver: pending_receiver, + density: SuperSimplex::new(seed), + hilly: SuperSimplex::new(seed.wrapping_add(1)), + stone: SuperSimplex::new(seed.wrapping_add(2)), + gravel: SuperSimplex::new(seed.wrapping_add(3)), + grass: SuperSimplex::new(seed.wrapping_add(4)), + }); + + // Chunks are generated in a thread pool for parallelism and to avoid blocking + // the main tick loop. You can use your thread pool of choice here (rayon, + // bevy_tasks, etc). Only the standard library is used in the example for the + // sake of simplicity. + // + // If your chunk generation algorithm is inexpensive then there's no need to do + // this. + for _ in 0..thread::available_parallelism().unwrap().get() { + let state = state.clone(); + thread::spawn(move || chunk_worker(state)); + } + + world.insert_resource(GameState { + pending: HashMap::new(), + sender: pending_sender, + receiver: finished_receiver, + }); + + let instance = world + .resource::() + .new_instance(DimensionId::default()); + + world.spawn(instance); } -struct Game { - player_count: AtomicUsize, - density_noise: SuperSimplex, - hilly_noise: SuperSimplex, - stone_noise: SuperSimplex, - gravel_noise: SuperSimplex, - grass_noise: SuperSimplex, +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, + mut commands: Commands, +) { + for mut client in &mut clients { + let instance = instances.single(); + + client.set_flat(true); + client.set_game_mode(GameMode::Creative); + client.set_position(SPAWN_POS); + client.set_instance(instance); + + commands.spawn(McEntity::with_uuid( + EntityKind::Player, + instance, + client.uuid(), + )); + } } -const MAX_PLAYERS: usize = 10; +fn remove_unviewed_chunks(mut instances: Query<&mut Instance>) { + instances + .single_mut() + .retain_chunks(|_, chunk| chunk.is_viewed_mut()); +} -#[async_trait] -impl Config for Game { - type ServerState = Option; - type ClientState = EntityId; - type EntityState = (); - type WorldState = (); - /// If the chunk should stay loaded at the end of the tick. - type ChunkState = bool; - type PlayerListState = (); - type InventoryState = (); +fn update_client_views( + mut instances: Query<&mut Instance>, + mut clients: Query<&mut Client>, + mut state: ResMut, +) { + let instance = instances.single_mut(); - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), + for client in &mut clients { + let view = client.view(); + let queue_pos = |pos| { + if instance.chunk(pos).is_none() { + match state.pending.entry(pos) { + Entry::Occupied(mut oe) => { + if let Some(priority) = oe.get_mut() { + let dist = view.pos.distance_squared(pos); + *priority = (*priority).min(dist); + } + } + Entry::Vacant(ve) => { + let dist = view.pos.distance_squared(pos); + ve.insert(Some(dist)); + } + } + } + }; + + // Queue all the new chunks in the view to be sent to the thread pool. + if client.is_added() { + view.iter().for_each(queue_pos); + } else { + let old_view = client.old_view(); + if old_view != view { + view.diff(old_view).for_each(queue_pos); + } + } + } +} + +fn send_recv_chunks(mut instances: Query<&mut Instance>, state: ResMut) { + let mut instance = instances.single_mut(); + let state = state.into_inner(); + + // Insert the chunks that are finished generating into the instance. + for (pos, chunk) in state.receiver.drain() { + instance.insert_chunk(pos, chunk); + assert!(state.pending.remove(&pos).is_some()); + } + + // Collect all the new chunks that need to be loaded this tick. + let mut to_send = vec![]; + + for (pos, priority) in &mut state.pending { + if let Some(pri) = priority.take() { + to_send.push((pri, pos)); } } - fn init(&self, server: &mut Server) { - server.worlds.insert(DimensionId::default(), ()); - server.state = Some(server.player_lists.insert(()).0); + // Sort chunks by ascending priority. + to_send.sort_unstable_by_key(|(pri, _)| *pri); + + // Send the sorted chunks to be loaded. + for (_, pos) in to_send { + let _ = state.sender.try_send(*pos); } +} - fn update(&self, server: &mut Server) { - let (world_id, world) = server.worlds.iter_mut().next().unwrap(); +fn chunk_worker(state: Arc) { + while let Ok(pos) = state.receiver.recv() { + let mut chunk = Chunk::new(SECTION_COUNT); - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } + for offset_z in 0..16 { + for offset_x in 0..16 { + let x = offset_x as i32 + pos.x * 16; + let z = offset_z as i32 + pos.z * 16; - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, entity)) => { - entity.set_world(world_id); - client.state = id - } - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } + let mut in_terrain = false; + let mut depth = 0; - client.respawn(world_id); - client.set_flat(true); - client.set_game_mode(GameMode::Creative); - client.teleport([0.0, 200.0, 0.0], 0.0, 0.0); - client.set_player_list(server.state.clone()); + // Fill in the terrain column. + for y in (0..chunk.section_count() as i32 * 16).rev() { + const WATER_HEIGHT: i32 = 55; - if let Some(id) = &server.state { - server.player_lists[id].insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } + let p = DVec3::new(x as f64, y as f64, z as f64); - client.send_message("Welcome to the terrain example!".italic()); - } + let block = if has_terrain_at(&state, p) { + let gravel_height = WATER_HEIGHT + - 1 + - (fbm(&state.gravel, p / 10.0, 3, 2.0, 0.5) * 6.0).floor() as i32; - let player = &mut server.entities[client.state]; - while let Some(event) = client.next_event() { - event.handle_default(client, player); - } - - let dist = client.view_distance(); - let p = client.position(); - - for pos in ChunkPos::at(p.x, p.z).in_view(dist) { - if let Some(chunk) = world.chunks.get_mut(pos) { - chunk.state = true; - } else { - world.chunks.insert(pos, UnloadedChunk::default(), true); - } - } - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - if let Some(id) = &server.state { - server.player_lists[id].remove(client.uuid()); - } - player.set_deleted(true); - - return false; - } - - true - }); - - // Remove chunks outside the view distance of players. - for (_, chunk) in world.chunks.iter_mut() { - chunk.set_deleted(!chunk.state); - chunk.state = false; - } - - // Generate chunk data for chunks created this tick. - world.chunks.par_iter_mut().for_each(|(pos, chunk)| { - if !chunk.created_this_tick() { - return; - } - - for z in 0..16 { - for x in 0..16 { - let block_x = x as i64 + pos.x as i64 * 16; - let block_z = z as i64 + pos.z as i64 * 16; - - let mut in_terrain = false; - let mut depth = 0; - - for y in (0..chunk.section_count() * 16).rev() { - let b = terrain_column( - self, - block_x, - y as i64, - block_z, - &mut in_terrain, - &mut depth, - ); - chunk.set_block_state(x, y, z, b); - } - - // Add grass - for y in (0..chunk.section_count() * 16).rev() { - if chunk.block_state(x, y, z).is_air() - && chunk.block_state(x, y - 1, z) == BlockState::GRASS_BLOCK - { - let density = fbm( - &self.grass_noise, - [block_x, y as i64, block_z].map(|a| a as f64 / 5.0), - 4, - 2.0, - 0.7, - ); - - if density > 0.55 { - if density > 0.7 && chunk.block_state(x, y + 1, z).is_air() { - let upper = BlockState::TALL_GRASS - .set(PropName::Half, PropValue::Upper); - let lower = BlockState::TALL_GRASS - .set(PropName::Half, PropValue::Lower); - - chunk.set_block_state(x, y + 1, z, upper); - chunk.set_block_state(x, y, z, lower); + if in_terrain { + if depth > 0 { + depth -= 1; + if y < gravel_height { + BlockState::GRAVEL } else { - chunk.set_block_state(x, y, z, BlockState::GRASS); + BlockState::DIRT } + } else { + BlockState::STONE + } + } else { + in_terrain = true; + let n = noise01(&state.stone, p / 15.0); + + depth = (n * 5.0).round() as u32; + + if y < gravel_height { + BlockState::GRAVEL + } else if y < WATER_HEIGHT - 1 { + BlockState::DIRT + } else { + BlockState::GRASS_BLOCK + } + } + } else { + in_terrain = false; + depth = 0; + if y < WATER_HEIGHT { + BlockState::WATER + } else { + BlockState::AIR + } + }; + + chunk.set_block_state(offset_x, y as usize, offset_z, block); + } + + // Add grass on top of grass blocks. + for y in (0..chunk.section_count() * 16).rev() { + if chunk.block_state(offset_x, y, offset_z).is_air() + && chunk.block_state(offset_x, y - 1, offset_z) == BlockState::GRASS_BLOCK + { + let p = DVec3::new(x as f64, y as f64, z as f64); + let density = fbm(&state.grass, p / 5.0, 4, 2.0, 0.7); + + if density > 0.55 { + if density > 0.7 + && chunk.block_state(offset_x, y + 1, offset_z).is_air() + { + let upper = + BlockState::TALL_GRASS.set(PropName::Half, PropValue::Upper); + let lower = + BlockState::TALL_GRASS.set(PropName::Half, PropValue::Lower); + + chunk.set_block_state(offset_x, y + 1, offset_z, upper); + chunk.set_block_state(offset_x, y, offset_z, lower); + } else { + chunk.set_block_state(offset_x, y, offset_z, BlockState::GRASS); } } } } } - }); + } + + let _ = state.sender.try_send((pos, chunk)); } } -fn terrain_column( - g: &Game, - x: i64, - y: i64, - z: i64, - in_terrain: &mut bool, - depth: &mut u32, -) -> BlockState { - const WATER_HEIGHT: i64 = 55; - - if has_terrain_at(g, x, y, z) { - let gravel_height = WATER_HEIGHT - - 1 - - (fbm( - &g.gravel_noise, - [x, y, z].map(|a| a as f64 / 10.0), - 3, - 2.0, - 0.5, - ) * 6.0) - .floor() as i64; - - if *in_terrain { - if *depth > 0 { - *depth -= 1; - if y < gravel_height { - BlockState::GRAVEL - } else { - BlockState::DIRT - } - } else { - BlockState::STONE - } - } else { - *in_terrain = true; - let n = noise01(&g.stone_noise, [x, y, z].map(|a| a as f64 / 15.0)); - - *depth = (n * 5.0).round() as u32; - - if y < gravel_height { - BlockState::GRAVEL - } else if y < WATER_HEIGHT - 1 { - BlockState::DIRT - } else { - BlockState::GRASS_BLOCK - } - } - } else { - *in_terrain = false; - *depth = 0; - if y < WATER_HEIGHT { - BlockState::WATER - } else { - BlockState::AIR - } - } -} - -fn has_terrain_at(g: &Game, x: i64, y: i64, z: i64) -> bool { - let hilly = Lerp::lerp_unclamped( - 0.1, - 1.0, - noise01(&g.hilly_noise, [x, y, z].map(|a| a as f64 / 400.0)).powi(2), - ); +fn has_terrain_at(state: &ChunkWorkerState, p: DVec3) -> bool { + let hilly = lerp(0.1, 1.0, noise01(&state.hilly, p / 400.0)).powi(2); let lower = 15.0 + 100.0 * hilly; let upper = lower + 100.0 * hilly; - if y as f64 <= lower { + if p.y <= lower { return true; - } else if y as f64 >= upper { + } else if p.y >= upper { return false; } - let density = 1.0 - lerpstep(lower, upper, y as f64); + let density = 1.0 - lerpstep(lower, upper, p.y); + + let n = fbm(&state.density, p / 100.0, 4, 2.0, 0.5); - let n = fbm( - &g.density_noise, - [x, y, z].map(|a| a as f64 / 100.0), - 4, - 2.0, - 0.5, - ); n < density } +fn lerp(a: f64, b: f64, t: f64) -> f64 { + a * (1.0 - t) + b * t +} + fn lerpstep(edge0: f64, edge1: f64, x: f64) -> f64 { if x <= edge0 { 0.0 @@ -318,14 +322,14 @@ fn lerpstep(edge0: f64, edge1: f64, x: f64) -> f64 { } } -fn fbm(noise: &SuperSimplex, p: [f64; 3], octaves: u32, lacunarity: f64, persistence: f64) -> f64 { +fn fbm(noise: &SuperSimplex, p: DVec3, octaves: u32, lacunarity: f64, persistence: f64) -> f64 { let mut freq = 1.0; let mut amp = 1.0; let mut amp_sum = 0.0; let mut sum = 0.0; for _ in 0..octaves { - let n = noise01(noise, p.map(|a| a * freq)); + let n = noise01(noise, p * freq); sum += n * amp; amp_sum += amp; @@ -337,6 +341,6 @@ fn fbm(noise: &SuperSimplex, p: [f64; 3], octaves: u32, lacunarity: f64, persist sum / amp_sum } -fn noise01(noise: &SuperSimplex, xyz: [f64; 3]) -> f64 { - (noise.get(xyz) + 1.0) / 2.0 +fn noise01(noise: &SuperSimplex, p: DVec3) -> f64 { + (noise.get(p.to_array()) + 1.0) / 2.0 } diff --git a/crates/valence/examples/text.rs b/crates/valence/examples/text.rs index a2de3cf..38b9674 100644 --- a/crates/valence/examples/text.rs +++ b/crates/valence/examples/text.rs @@ -1,213 +1,125 @@ -use std::net::SocketAddr; - +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; use valence::prelude::*; +use valence_protocol::translation_key; -pub fn main() -> ShutdownResult { +const SPAWN_Y: i32 = 64; + +pub fn main() { tracing_subscriber::fmt().init(); - valence::start_server(Game::default(), ServerState::default()) + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_set(PlayerList::default_system_set()) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .run(); } -#[derive(Default)] -struct Game {} +fn setup(world: &mut World) { + let mut instance = world + .resource::() + .new_instance(DimensionId::default()); -#[derive(Default)] -struct ClientState { - entity_id: EntityId, -} - -#[derive(Default)] -struct ServerState { - world: WorldId, -} - -const FLOOR_Y: i32 = 1; -const PLATFORM_X: i32 = 3; -const PLATFORM_Z: i32 = 3; -const SPAWN_POS: Vec3 = Vec3::new(1.5, 2.0, 1.5); - -#[async_trait] -impl Config for Game { - type ServerState = ServerState; - type ClientState = ClientState; - type EntityState = (); - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - type InventoryState = (); - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: -1, - max_players: -1, - description: "Hello Valence! ".into_text() + "Text Example".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), - player_sample: Default::default(), + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); } } - fn init(&self, server: &mut Server) { - server.state = ServerState { - world: create_world(server), - }; - } - - fn update(&self, server: &mut Server) { - server.clients.retain(|_, client| { - if client.created_this_tick() { - // Boilerplate for client initialization - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, _)) => client.entity_id = id, - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } - - let world_id = server.state.world; - - client.set_flat(true); - client.respawn(world_id); - client.teleport(SPAWN_POS, -90.0, 0.0); - client.set_game_mode(GameMode::Creative); - - client.send_message("Welcome to the text example.".bold()); - client.send_message( - "The following examples show ways to use the different text components.", - ); - - // Text examples - client.send_message("\nText"); - client.send_message(" - ".into_text() + Text::text("Plain text")); - client.send_message(" - ".into_text() + Text::text("Styled text").italic()); - client.send_message( - " - ".into_text() + Text::text("Colored text").color(Color::GOLD), - ); - client.send_message( - " - ".into_text() - + Text::text("Colored and styled text") - .color(Color::GOLD) - .italic() - .underlined(), - ); - - // Translated text examples - client.send_message("\nTranslated Text"); - client.send_message( - " - 'chat.type.advancement.task': ".into_text() - + Text::translate(translation_key::CHAT_TYPE_ADVANCEMENT_TASK, []), - ); - client.send_message( - " - 'chat.type.advancement.task' with slots: ".into_text() - + Text::translate( - translation_key::CHAT_TYPE_ADVANCEMENT_TASK, - ["arg1".into(), "arg2".into()], - ), - ); - client.send_message( - " - 'custom.translation_key': ".into_text() - + Text::translate("custom.translation_key", []), - ); - - // Scoreboard value example - client.send_message("\nScoreboard Values"); - client.send_message(" - Score: ".into_text() + Text::score("*", "objective", None)); - client.send_message( - " - Score with custom value: ".into_text() - + Text::score("*", "objective", Some("value".into())), - ); - - // Entity names example - client.send_message("\nEntity Names (Selector)"); - client.send_message(" - Nearest player: ".into_text() + Text::selector("@p", None)); - client.send_message(" - Random player: ".into_text() + Text::selector("@r", None)); - client.send_message(" - All players: ".into_text() + Text::selector("@a", None)); - client.send_message(" - All entities: ".into_text() + Text::selector("@e", None)); - client.send_message( - " - All entities with custom separator: ".into_text() - + Text::selector("@e", Some(", ".into_text().color(Color::GOLD))), - ); - - // Keybind example - client.send_message("\nKeybind"); - client.send_message( - " - 'key.inventory': ".into_text() + Text::keybind("key.inventory"), - ); - - // NBT examples - client.send_message("\nNBT"); - client.send_message( - " - Block NBT: ".into_text() + Text::block_nbt("{}", "0 1 0", None, None), - ); - client.send_message( - " - Entity NBT: ".into_text() + Text::entity_nbt("{}", "@a", None, None), - ); - client.send_message( - " - Storage NBT: ".into_text() - + Text::storage_nbt(ident!("storage.key"), "@a", None, None), - ); - - client.send_message( - "\n\n↑ ".into_text().bold().color(Color::GOLD) - + "Scroll up to see the full example!".into_text().not_bold(), - ); - } - - if client.position().y < 0.0 { - client.teleport(SPAWN_POS, 0.0, 0.0); - } - - let player = server.entities.get_mut(client.entity_id).unwrap(); - - while let Some(event) = client.next_event() { - event.handle_default(client, player); - } - - if client.is_disconnected() { - player.set_deleted(true); - return false; - } - - true - }); - } -} - -// Boilerplate for creating world -fn create_world(server: &mut Server) -> WorldId { - let dimension = server.shared.dimensions().next().unwrap(); - - let (world_id, world) = server.worlds.insert(dimension.0, ()); - - // Create chunks - for z in -3..3 { - for x in -3..3 { - world.chunks.insert([x, z], UnloadedChunk::default(), ()); + for z in -25..25 { + for x in -25..25 { + instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } - // Create platform - let platform_block = BlockState::GLASS; - - for z in 0..PLATFORM_Z { - for x in 0..PLATFORM_X { - world - .chunks - .set_block_state([x, FLOOR_Y, z], platform_block); - } - } - - world_id + world.spawn(instance); +} + +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, +) { + for mut client in &mut clients { + client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + client.set_instance(instances.single()); + client.set_game_mode(GameMode::Creative); + + client.send_message("Welcome to the text example.".bold()); + client + .send_message("The following examples show ways to use the different text components."); + + // Text examples + client.send_message("\nText"); + client.send_message(" - ".into_text() + Text::text("Plain text")); + client.send_message(" - ".into_text() + Text::text("Styled text").italic()); + client.send_message(" - ".into_text() + Text::text("Colored text").color(Color::GOLD)); + client.send_message( + " - ".into_text() + + Text::text("Colored and styled text") + .color(Color::GOLD) + .italic() + .underlined(), + ); + + // Translated text examples + client.send_message("\nTranslated Text"); + client.send_message( + " - 'chat.type.advancement.task': ".into_text() + + Text::translate(translation_key::CHAT_TYPE_ADVANCEMENT_TASK, []), + ); + client.send_message( + " - 'chat.type.advancement.task' with slots: ".into_text() + + Text::translate( + translation_key::CHAT_TYPE_ADVANCEMENT_TASK, + ["arg1".into(), "arg2".into()], + ), + ); + client.send_message( + " - 'custom.translation_key': ".into_text() + + Text::translate("custom.translation_key", []), + ); + + // Scoreboard value example + client.send_message("\nScoreboard Values"); + client.send_message(" - Score: ".into_text() + Text::score("*", "objective", None)); + client.send_message( + " - Score with custom value: ".into_text() + + Text::score("*", "objective", Some("value".into())), + ); + + // Entity names example + client.send_message("\nEntity Names (Selector)"); + client.send_message(" - Nearest player: ".into_text() + Text::selector("@p", None)); + client.send_message(" - Random player: ".into_text() + Text::selector("@r", None)); + client.send_message(" - All players: ".into_text() + Text::selector("@a", None)); + client.send_message(" - All entities: ".into_text() + Text::selector("@e", None)); + client.send_message( + " - All entities with custom separator: ".into_text() + + Text::selector("@e", Some(", ".into_text().color(Color::GOLD))), + ); + + // Keybind example + client.send_message("\nKeybind"); + client.send_message(" - 'key.inventory': ".into_text() + Text::keybind("key.inventory")); + + // NBT examples + client.send_message("\nNBT"); + client.send_message( + " - Block NBT: ".into_text() + Text::block_nbt("{}", "0 1 0", None, None), + ); + client + .send_message(" - Entity NBT: ".into_text() + Text::entity_nbt("{}", "@a", None, None)); + client.send_message( + " - Storage NBT: ".into_text() + + Text::storage_nbt(ident!("storage.key"), "@a", None, None), + ); + + client.send_message( + "\n\n↑ ".into_text().bold().color(Color::GOLD) + + "Scroll up to see the full example!".into_text().not_bold(), + ); + } } diff --git a/crates/valence/src/biome.rs b/crates/valence/src/biome.rs index 676a6f3..1b603ca 100644 --- a/crates/valence/src/biome.rs +++ b/crates/valence/src/biome.rs @@ -10,18 +10,21 @@ use valence_protocol::ident::Ident; /// Identifies a particular [`Biome`] on the server. /// -/// The default biome ID refers to the first biome added in the server's -/// [configuration](crate::config::Config). +/// The default biome ID refers to the first biome added in +/// [`ServerPlugin::biomes`]. /// -/// To obtain biome IDs for other biomes, call -/// [`biomes`](crate::server::SharedServer::biomes). +/// To obtain biome IDs for other biomes, see [`ServerPlugin::biomes`]. +/// +/// [`ServerPlugin::biomes`]: crate::config::ServerPlugin::biomes #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct BiomeId(pub(crate) u16); /// Contains the configuration for a biome. /// /// Biomes are registered once at startup through -/// [`biomes`](crate::config::Config::biomes). +/// [`ServerPlugin::with_biomes`] +/// +/// [`ServerPlugin::with_biomes`]: crate::config::ServerPlugin::with_biomes #[derive(Clone, Debug)] pub struct Biome { /// The unique name for this biome. The name can be @@ -40,8 +43,7 @@ pub struct Biome { pub additions_sound: Option, pub mood_sound: Option, pub particle: Option, - // TODO: The following fields should be added if they can affect the appearance of the biome to - // clients. + // TODO // * depth: f32 // * temperature: f32 // * scale: f32 diff --git a/crates/valence/src/chunk.rs b/crates/valence/src/chunk.rs deleted file mode 100644 index 81712be..0000000 --- a/crates/valence/src/chunk.rs +++ /dev/null @@ -1,1101 +0,0 @@ -//! Chunks and related types. -//! -//! A chunk is a 16x16-block segment of a world with a height determined by the -//! [`Dimension`](crate::dimension::Dimension) of the world. -//! -//! In addition to blocks, chunks also contain [biomes](crate::biome::Biome). -//! Every 4x4x4 segment of blocks in a chunk corresponds to a biome. - -use std::collections::hash_map::Entry; -use std::io::Write; -use std::iter::FusedIterator; -use std::mem; -use std::ops::{Deref, DerefMut, Index, IndexMut}; -use std::sync::{Mutex, MutexGuard}; - -use entity_partition::PartitionCell; -use paletted_container::PalettedContainer; -pub use pos::ChunkPos; -use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator}; -use rustc_hash::FxHashMap; -use valence_nbt::compound; -use valence_protocol::packets::s2c::play::{ - BlockUpdate, ChunkDataAndUpdateLightEncode, UpdateSectionBlocksEncode, -}; -use valence_protocol::{BlockPos, BlockState, Encode, LengthPrefixedArray, VarInt, VarLong}; - -use crate::biome::BiomeId; -use crate::config::Config; -use crate::packet::{PacketWriter, WritePacket}; -use crate::util::bit_width; - -pub(crate) mod entity_partition; -mod paletted_container; -mod pos; - -/// A container for all [`LoadedChunk`]s in a [`World`](crate::world::World). -pub struct Chunks { - /// Maps chunk positions to chunks. We store both loaded chunks and - /// partition cells here so we can get both in a single hashmap lookup - /// during the client update procedure. - chunks: FxHashMap>, PartitionCell)>, - dimension_height: i32, - dimension_min_y: i32, - filler_sky_light_mask: Box<[u64]>, - /// Sending filler light data causes the vanilla client to lag - /// less. Hopefully we can remove this in the future. - filler_sky_light_arrays: Box<[LengthPrefixedArray]>, - biome_registry_len: usize, - compression_threshold: Option, -} - -impl Chunks { - pub(crate) fn new( - dimension_height: i32, - dimension_min_y: i32, - biome_registry_len: usize, - compression_threshold: Option, - ) -> Self { - let section_count = (dimension_height / 16 + 2) as usize; - - let mut sky_light_mask = vec![0; num::Integer::div_ceil(§ion_count, &16)]; - - for i in 0..section_count { - sky_light_mask[i / 64] |= 1 << (i % 64); - } - - Self { - chunks: FxHashMap::default(), - dimension_height, - dimension_min_y, - filler_sky_light_mask: sky_light_mask.into(), - filler_sky_light_arrays: vec![LengthPrefixedArray([0xff; 2048]); section_count].into(), - biome_registry_len, - compression_threshold, - } - } - - /// Consumes an [`UnloadedChunk`] and creates a [`LoadedChunk`] at a given - /// position. An exclusive reference to the new chunk is returned. - /// - /// If a chunk at the position already exists, then the old chunk - /// is overwritten and its contents are dropped. - /// - /// The given chunk is resized to match the height of the world as if by - /// calling [`UnloadedChunk::resize`]. - /// - /// **Note**: For the vanilla Minecraft client to see a chunk, all chunks - /// adjacent to it must also be loaded. Clients should not be spawned within - /// unloaded chunks via [`respawn`](crate::client::Client::respawn). - pub fn insert( - &mut self, - pos: impl Into, - chunk: UnloadedChunk, - state: C::ChunkState, - ) -> &mut LoadedChunk { - let dimension_section_count = (self.dimension_height / 16) as usize; - let loaded = LoadedChunk::new(chunk, dimension_section_count, state); - - match self.chunks.entry(pos.into()) { - Entry::Occupied(mut oe) => { - oe.get_mut().0 = Some(loaded); - oe.into_mut().0.as_mut().unwrap() - } - Entry::Vacant(ve) => ve - .insert((Some(loaded), PartitionCell::new())) - .0 - .as_mut() - .unwrap(), - } - } - - /// Returns the height of all loaded chunks in the world. This returns the - /// same value as [`Chunk::section_count`] multiplied by 16 for all loaded - /// chunks. - pub fn height(&self) -> usize { - self.dimension_height as usize - } - - /// The minimum Y coordinate in world space that chunks in this world can - /// occupy. This is relevant for [`Chunks::block_state`] and - /// [`Chunks::set_block_state`]. - pub fn min_y(&self) -> i32 { - self.dimension_min_y - } - - /// Gets a shared reference to the chunk at the provided position. - /// - /// If there is no chunk at the position, then `None` is returned. - pub fn get(&self, pos: impl Into) -> Option<&LoadedChunk> { - self.chunks.get(&pos.into())?.0.as_ref() - } - - pub(crate) fn chunk_and_cell( - &self, - pos: ChunkPos, - ) -> Option<&(Option>, PartitionCell)> { - self.chunks.get(&pos) - } - - fn cell_mut(&mut self, pos: ChunkPos) -> Option<&mut PartitionCell> { - self.chunks.get_mut(&pos).map(|(_, cell)| cell) - } - - /// Gets an exclusive reference to the chunk at the provided position. - /// - /// If there is no chunk at the position, then `None` is returned. - pub fn get_mut(&mut self, pos: impl Into) -> Option<&mut LoadedChunk> { - self.chunks.get_mut(&pos.into())?.0.as_mut() - } - - /// Returns an iterator over all chunks in the world in an unspecified - /// order. - pub fn iter(&self) -> impl FusedIterator)> + Clone + '_ { - self.chunks - .iter() - .filter_map(|(&pos, (chunk, _))| chunk.as_ref().map(|c| (pos, c))) - } - - /// Returns a mutable iterator over all chunks in the world in an - /// unspecified order. - pub fn iter_mut(&mut self) -> impl FusedIterator)> + '_ { - self.chunks - .iter_mut() - .filter_map(|(&pos, (chunk, _))| chunk.as_mut().map(|c| (pos, c))) - } - - fn cells_mut( - &mut self, - ) -> impl ExactSizeIterator + FusedIterator + '_ { - self.chunks.iter_mut().map(|(_, (_, cell))| cell) - } - - /// Returns a parallel iterator over all chunks in the world in an - /// unspecified order. - pub fn par_iter( - &self, - ) -> impl ParallelIterator)> + Clone + '_ { - self.chunks - .par_iter() - .filter_map(|(&pos, (chunk, _))| chunk.as_ref().map(|c| (pos, c))) - } - - /// Returns a parallel mutable iterator over all chunks in the world in an - /// unspecified order. - pub fn par_iter_mut( - &mut self, - ) -> impl ParallelIterator)> + '_ { - self.chunks - .par_iter_mut() - .filter_map(|(&pos, (chunk, _))| chunk.as_mut().map(|c| (pos, c))) - } - - /// Gets the block state at an absolute block position in world space. - /// - /// If the position is not inside of a chunk, then `None` is returned. - /// - /// **Note**: if you need to get a large number of blocks, it is more - /// efficient to read from the chunks directly with - /// [`Chunk::block_state`]. - pub fn block_state(&self, pos: impl Into) -> Option { - let pos = pos.into(); - let chunk_pos = ChunkPos::from(pos); - - let chunk = self.get(chunk_pos)?; - - let y = pos.y.checked_sub(self.dimension_min_y)?.try_into().ok()?; - - if y < chunk.section_count() * 16 { - Some(chunk.block_state( - pos.x.rem_euclid(16) as usize, - y, - pos.z.rem_euclid(16) as usize, - )) - } else { - None - } - } - - /// Sets the block state at an absolute block position in world space. The - /// previous block state at the position is returned. - /// - /// If the given position is not inside of a loaded chunk, then a new chunk - /// is created at the position before the block is set. - /// - /// If the position is completely out of bounds, then no new chunk is - /// created and [`BlockState::AIR`] is returned. - pub fn set_block_state(&mut self, pos: impl Into, block: BlockState) -> BlockState - where - C::ChunkState: Default, - { - let pos = pos.into(); - - let Some(y) = pos.y.checked_sub(self.dimension_min_y).and_then(|y| y.try_into().ok()) else { - return BlockState::AIR; - }; - - if y >= self.dimension_height as usize { - return BlockState::AIR; - } - - let chunk = match self.chunks.entry(ChunkPos::from(pos)) { - Entry::Occupied(oe) => oe.into_mut().0.get_or_insert_with(|| { - let dimension_section_count = (self.dimension_height / 16) as usize; - LoadedChunk::new( - UnloadedChunk::default(), - dimension_section_count, - Default::default(), - ) - }), - Entry::Vacant(ve) => { - let dimension_section_count = (self.dimension_height / 16) as usize; - let loaded = LoadedChunk::new( - UnloadedChunk::default(), - dimension_section_count, - Default::default(), - ); - - ve.insert((Some(loaded), PartitionCell::new())) - .0 - .as_mut() - .unwrap() - } - }; - - chunk.set_block_state( - pos.x.rem_euclid(16) as usize, - y, - pos.z.rem_euclid(16) as usize, - block, - ) - } - - pub(crate) fn update_caches(&mut self) { - let min_y = self.dimension_min_y; - - self.chunks.par_iter_mut().for_each(|(&pos, (chunk, _))| { - let Some(chunk) = chunk else { - // There is no chunk at this position. - return; - }; - - if chunk.deleted { - // Deleted chunks are not sending packets to anyone. - return; - } - - let mut compression_scratch = vec![]; - let mut blocks = vec![]; - - chunk.cached_update_packets.clear(); - let mut any_blocks_modified = false; - - for (sect_y, sect) in chunk.sections.iter_mut().enumerate() { - let modified_blocks_count: u32 = sect - .modified_blocks - .iter() - .map(|&bits| bits.count_ones()) - .sum(); - - // If the chunk is created this tick, clients are only going to be sent the - // chunk data packet so there is no need to cache the modified blocks packets. - if !chunk.created_this_tick { - if modified_blocks_count == 1 { - let (i, bits) = sect - .modified_blocks - .iter() - .cloned() - .enumerate() - .find(|(_, n)| *n > 0) - .expect("invalid modified count"); - - debug_assert_eq!(bits.count_ones(), 1); - - let idx = i * USIZE_BITS + bits.trailing_zeros() as usize; - let block = sect.block_states.get(idx); - - let global_x = pos.x * 16 + (idx % 16) as i32; - let global_y = sect_y as i32 * 16 + (idx / (16 * 16)) as i32 + min_y; - let global_z = pos.z * 16 + (idx / 16 % 16) as i32; - - let mut writer = PacketWriter::new( - &mut chunk.cached_update_packets, - self.compression_threshold, - &mut compression_scratch, - ); - - writer - .write_packet(&BlockUpdate { - position: BlockPos::new(global_x, global_y, global_z), - block_id: VarInt(block.to_raw() as _), - }) - .unwrap(); - } else if modified_blocks_count > 1 { - blocks.clear(); - - for y in 0..16 { - for z in 0..16 { - for x in 0..16 { - let idx = x as usize + z as usize * 16 + y as usize * 16 * 16; - - if sect.is_block_modified(idx) { - let block_id = sect.block_states.get(idx).to_raw(); - let compact = - (block_id as i64) << 12 | (x << 8 | z << 4 | y); - - blocks.push(VarLong(compact)); - } - } - } - } - - let chunk_section_position = (pos.x as i64) << 42 - | (pos.z as i64 & 0x3fffff) << 20 - | (sect_y as i64 + min_y.div_euclid(16) as i64) & 0xfffff; - - let mut writer = PacketWriter::new( - &mut chunk.cached_update_packets, - self.compression_threshold, - &mut compression_scratch, - ); - - writer - .write_packet(&UpdateSectionBlocksEncode { - chunk_section_position, - invert_trust_edges: false, - blocks: &blocks, - }) - .unwrap(); - } - } - - if modified_blocks_count > 0 { - any_blocks_modified = true; - sect.modified_blocks.fill(0); - } - } - - // Clear the cache if the cache was invalidated. - if any_blocks_modified || chunk.any_biomes_modified { - chunk.any_biomes_modified = false; - chunk.cached_init_packet.get_mut().unwrap().clear(); - } - - // Initialize the chunk data cache on new chunks here so this work can be done - // in parallel. - if chunk.created_this_tick() { - debug_assert!(chunk.cached_init_packet.get_mut().unwrap().is_empty()); - - let _unused: MutexGuard<_> = chunk.get_chunk_data_packet( - &mut compression_scratch, - pos, - self.biome_registry_len, - &self.filler_sky_light_mask, - &self.filler_sky_light_arrays, - self.compression_threshold, - ); - } - }); - } - - /// Clears changes to partition cells and removes deleted chunks and - /// partition cells. - pub(crate) fn update(&mut self) { - self.chunks.retain(|_, (chunk_opt, cell)| { - if let Some(chunk) = chunk_opt { - if chunk.deleted { - *chunk_opt = None; - } else { - chunk.created_this_tick = false; - } - } - - cell.clear_incoming_outgoing(); - - chunk_opt.is_some() || cell.entities().len() > 0 - }); - } -} - -impl> Index

for Chunks { - type Output = LoadedChunk; - - fn index(&self, index: P) -> &Self::Output { - let ChunkPos { x, z } = index.into(); - self.get((x, z)) - .unwrap_or_else(|| panic!("missing chunk at ({x}, {z})")) - } -} - -impl> IndexMut

for Chunks { - fn index_mut(&mut self, index: P) -> &mut Self::Output { - let ChunkPos { x, z } = index.into(); - self.get_mut((x, z)) - .unwrap_or_else(|| panic!("missing chunk at ({x}, {z})")) - } -} - -/// Operations that can be performed on a chunk. [`LoadedChunk`] and -/// [`UnloadedChunk`] implement this trait. -pub trait Chunk { - /// Returns the number of sections in this chunk. To get the height of the - /// chunk in meters, multiply the result by 16. - fn section_count(&self) -> usize; - - /// Gets the block state at the provided offsets in the chunk. - /// - /// **Note**: The arguments to this function are offsets from the minimum - /// corner of the chunk in _chunk space_ rather than _world space_. - /// - /// # Panics - /// - /// Panics if the offsets are outside the bounds of the chunk. `x` and `z` - /// must be less than 16 while `y` must be less than `section_count() * 16`. - fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState; - - /// Sets the block state at the provided offsets in the chunk. The previous - /// block state at the position is returned. - /// - /// **Note**: The arguments to this function are offsets from the minimum - /// corner of the chunk in _chunk space_ rather than _world space_. - /// - /// # Panics - /// - /// Panics if the offsets are outside the bounds of the chunk. `x` and `z` - /// must be less than 16 while `y` must be less than `section_count() * 16`. - fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState; - - /// Sets every block in a section to the given block state. - /// - /// This is semantically equivalent to setting every block in the section - /// with [`set_block_state`]. However, this function may be implemented more - /// efficiently. - /// - /// # Panics - /// - /// Panics if `sect_y` is out of bounds. `sect_y` must be less than the - /// section count. - /// - /// [`set_block_state`]: Self::set_block_state - fn fill_block_states(&mut self, sect_y: usize, block: BlockState); - - /// Gets the biome at the provided biome offsets in the chunk. - /// - /// **Note**: the arguments are **not** block positions. Biomes are 4x4x4 - /// segments of a chunk, so `x` and `z` are in `0..4`. - /// - /// # Panics - /// - /// Panics if the offsets are outside the bounds of the chunk. `x` and `z` - /// must be less than 4 while `y` must be less than `section_count() * 4`. - fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId; - - /// Sets the biome at the provided offsets in the chunk. The previous - /// biome at the position is returned. - /// - /// **Note**: the arguments are **not** block positions. Biomes are 4x4x4 - /// segments of a chunk, so `x` and `z` are in `0..4`. - /// - /// # Panics - /// - /// Panics if the offsets are outside the bounds of the chunk. `x` and `z` - /// must be less than 4 while `y` must be less than `section_count() * 4`. - fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId; - - /// Sets every biome in a section to the given block state. - /// - /// This is semantically equivalent to setting every biome in the section - /// with [`set_biome`]. However, this function may be implemented more - /// efficiently. - /// - /// # Panics - /// - /// Panics if `sect_y` is out of bounds. `sect_y` must be less than the - /// section count. - /// - /// [`set_biome`]: Self::set_biome - fn fill_biomes(&mut self, sect_y: usize, biome: BiomeId); - - /// Optimizes this chunk to use the minimum amount of memory possible. It - /// should have no observable effect on the contents of the chunk. - /// - /// This is a potentially expensive operation. The function is most - /// effective when a large number of blocks and biomes have changed states. - fn optimize(&mut self); -} - -/// A chunk that is not loaded in any world. -pub struct UnloadedChunk { - sections: Vec, - // TODO: block_entities: BTreeMap, -} - -impl UnloadedChunk { - /// Constructs a new unloaded chunk containing only [`BlockState::AIR`] and - /// [`BiomeId::default()`] with the given number of sections. A section is a - /// 16x16x16 meter volume. - pub fn new(section_count: usize) -> Self { - let mut chunk = Self { sections: vec![] }; - chunk.resize(section_count); - chunk - } - - /// Changes the section count of the chunk to `new_section_count`. This is a - /// potentially expensive operation that may involve copying. - /// - /// The chunk is extended and truncated from the top. New blocks are always - /// [`BlockState::AIR`] and biomes are [`BiomeId::default()`]. - pub fn resize(&mut self, new_section_count: usize) { - let old_section_count = self.section_count(); - - if new_section_count > old_section_count { - self.sections - .reserve_exact(new_section_count - old_section_count); - self.sections - .resize_with(new_section_count, ChunkSection::default); - debug_assert_eq!(self.sections.capacity(), self.sections.len()); - } else { - self.sections.truncate(new_section_count); - } - } -} - -/// Constructs a new chunk with height `0`. -impl Default for UnloadedChunk { - fn default() -> Self { - Self::new(0) - } -} - -impl Chunk for UnloadedChunk { - fn section_count(&self) -> usize { - self.sections.len() - } - - fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState { - assert!( - x < 16 && y < self.section_count() * 16 && z < 16, - "chunk block offsets of ({x}, {y}, {z}) are out of bounds" - ); - - self.sections[y / 16] - .block_states - .get(x + z * 16 + y % 16 * 16 * 16) - } - - fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState { - assert!( - x < 16 && y < self.section_count() * 16 && z < 16, - "chunk block offsets of ({x}, {y}, {z}) are out of bounds" - ); - - let mut sect = &mut self.sections[y / 16]; - - let old_block = sect.block_states.set(x + z * 16 + y % 16 * 16 * 16, block); - - match (block.is_air(), old_block.is_air()) { - (true, false) => sect.non_air_count -= 1, - (false, true) => sect.non_air_count += 1, - _ => {} - } - - old_block - } - - fn fill_block_states(&mut self, sect_y: usize, block: BlockState) { - let Some(sect) = self.sections.get_mut(sect_y) else { - panic!( - "section index {sect_y} out of bounds for chunk with {} sections", - self.section_count() - ) - }; - - if block.is_air() { - sect.non_air_count = 0; - } else { - sect.non_air_count = SECTION_BLOCK_COUNT as u16; - } - - sect.block_states.fill(block); - } - - fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId { - assert!( - x < 4 && y < self.section_count() * 4 && z < 4, - "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" - ); - - self.sections[y / 4].biomes.get(x + z * 4 + y % 4 * 4 * 4) - } - - fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId { - assert!( - x < 4 && y < self.section_count() * 4 && z < 4, - "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" - ); - - self.sections[y / 4] - .biomes - .set(x + z * 4 + y % 4 * 4 * 4, biome) - } - - fn fill_biomes(&mut self, sect_y: usize, biome: BiomeId) { - let Some(sect) = self.sections.get_mut(sect_y) else { - panic!( - "section index {sect_y} out of bounds for chunk with {} sections", - self.section_count() - ) - }; - - sect.biomes.fill(biome); - } - - fn optimize(&mut self) { - for sect in self.sections.iter_mut() { - sect.block_states.optimize(); - sect.biomes.optimize(); - } - } -} - -/// A chunk which is currently loaded in a world. -pub struct LoadedChunk { - /// Custom state. - pub state: C::ChunkState, - sections: Box<[ChunkSection]>, - // TODO: block_entities: BTreeMap, - cached_init_packet: Mutex>, - cached_update_packets: Vec, - /// If any of the biomes in this chunk were modified this tick. - any_biomes_modified: bool, - created_this_tick: bool, - deleted: bool, - /// For debugging purposes. - #[cfg(debug_assertions)] - uuid: uuid::Uuid, -} - -impl Deref for LoadedChunk { - type Target = C::ChunkState; - - fn deref(&self) -> &Self::Target { - &self.state - } -} - -impl DerefMut for LoadedChunk { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.state - } -} - -/// A 16x16x16 meter volume of blocks, biomes, and light in a chunk. -#[derive(Clone, Debug)] -struct ChunkSection { - block_states: PalettedContainer, - /// Contains a set bit for every block that has been modified in this - /// section this tick. Ignored in unloaded chunks. - modified_blocks: [usize; SECTION_BLOCK_COUNT / USIZE_BITS], - /// Number of non-air blocks in this section. - non_air_count: u16, - biomes: PalettedContainer, -} - -// [T; 64] Doesn't implement Default so we can't derive :( -impl Default for ChunkSection { - fn default() -> Self { - Self { - block_states: Default::default(), - modified_blocks: [0; SECTION_BLOCK_COUNT / USIZE_BITS], - non_air_count: 0, - biomes: Default::default(), - } - } -} - -const SECTION_BLOCK_COUNT: usize = 4096; -const USIZE_BITS: usize = usize::BITS as _; - -impl ChunkSection { - fn mark_block_as_modified(&mut self, idx: usize) { - self.modified_blocks[idx / USIZE_BITS] |= 1 << (idx % USIZE_BITS); - } - - fn mark_all_blocks_as_modified(&mut self) { - self.modified_blocks.fill(usize::MAX); - } - - fn is_block_modified(&self, idx: usize) -> bool { - self.modified_blocks[idx / USIZE_BITS] >> (idx % USIZE_BITS) & 1 == 1 - } -} - -impl LoadedChunk { - fn new(mut chunk: UnloadedChunk, dimension_section_count: usize, state: C::ChunkState) -> Self { - chunk.resize(dimension_section_count); - - Self { - state, - sections: chunk.sections.into(), - cached_init_packet: Mutex::new(vec![]), - cached_update_packets: vec![], - any_biomes_modified: false, - created_this_tick: true, - deleted: false, - #[cfg(debug_assertions)] - uuid: uuid::Uuid::from_u128(rand::random()), - } - } - - pub fn take(&mut self) -> UnloadedChunk { - let unloaded = UnloadedChunk { - sections: mem::take(&mut self.sections).into(), - }; - - self.created_this_tick = true; - - unloaded - } - - /// Returns `true` if this chunk was created during the current tick. - pub fn created_this_tick(&self) -> bool { - self.created_this_tick - } - - pub fn deleted(&self) -> bool { - self.deleted - } - - pub fn set_deleted(&mut self, deleted: bool) { - self.deleted = deleted; - } - - /// Queues the chunk data packet for this chunk with the given position. - /// This will initialize the chunk for the client. - pub(crate) fn write_chunk_data_packet( - &self, - mut writer: impl WritePacket, - scratch: &mut Vec, - pos: ChunkPos, - chunks: &Chunks, - ) -> anyhow::Result<()> { - #[cfg(debug_assertions)] - assert_eq!( - chunks[pos].uuid, self.uuid, - "chunks and/or position arguments are incorrect" - ); - - let bytes = self.get_chunk_data_packet( - scratch, - pos, - chunks.biome_registry_len, - &chunks.filler_sky_light_mask, - &chunks.filler_sky_light_arrays, - chunks.compression_threshold, - ); - - writer.write_bytes(&bytes) - } - - /// Gets the bytes of the cached chunk data packet, initializing the cache - /// if it is empty. - fn get_chunk_data_packet( - &self, - scratch: &mut Vec, - pos: ChunkPos, - biome_registry_len: usize, - filler_sky_light_mask: &[u64], - filler_sky_light_arrays: &[LengthPrefixedArray], - compression_threshold: Option, - ) -> MutexGuard> { - let mut lck = self.cached_init_packet.lock().unwrap(); - - if lck.is_empty() { - scratch.clear(); - - for sect in self.sections.iter() { - sect.non_air_count.encode(&mut *scratch).unwrap(); - - sect.block_states - .encode_mc_format( - &mut *scratch, - |b| b.to_raw().into(), - 4, - 8, - bit_width(BlockState::max_raw().into()), - ) - .unwrap(); - - sect.biomes - .encode_mc_format( - &mut *scratch, - |b| b.0.into(), - 0, - 3, - bit_width(biome_registry_len - 1), - ) - .unwrap(); - } - - let mut compression_scratch = vec![]; - - let mut writer = - PacketWriter::new(&mut lck, compression_threshold, &mut compression_scratch); - - writer - .write_packet(&ChunkDataAndUpdateLightEncode { - chunk_x: pos.x, - chunk_z: pos.z, - heightmaps: &compound! { - // TODO: MOTION_BLOCKING heightmap - }, - blocks_and_biomes: scratch, - block_entities: &[], - trust_edges: true, - sky_light_mask: filler_sky_light_mask, - block_light_mask: &[], - empty_sky_light_mask: &[], - empty_block_light_mask: &[], - sky_light_arrays: filler_sky_light_arrays, - block_light_arrays: &[], - }) - .unwrap(); - } - - lck - } - - /// Queues block change packets for this chunk. - pub(crate) fn write_block_change_packets( - &self, - mut writer: impl WritePacket, - ) -> anyhow::Result<()> { - writer.write_bytes(&self.cached_update_packets) - } -} - -impl Chunk for LoadedChunk { - fn section_count(&self) -> usize { - self.sections.len() - } - - fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState { - assert!( - x < 16 && y < self.section_count() * 16 && z < 16, - "chunk block offsets of ({x}, {y}, {z}) are out of bounds" - ); - - self.sections[y / 16] - .block_states - .get(x + z * 16 + y % 16 * 16 * 16) - } - - fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState { - assert!( - x < 16 && y < self.section_count() * 16 && z < 16, - "chunk block offsets of ({x}, {y}, {z}) are out of bounds" - ); - - let sect = &mut self.sections[y / 16]; - let idx = x + z * 16 + y % 16 * 16 * 16; - - let old_block = sect.block_states.set(idx, block); - - if block != old_block { - match (block.is_air(), old_block.is_air()) { - (true, false) => sect.non_air_count -= 1, - (false, true) => sect.non_air_count += 1, - _ => {} - } - - sect.mark_block_as_modified(idx); - } - - old_block - } - - fn fill_block_states(&mut self, sect_y: usize, block: BlockState) { - let Some(sect) = self.sections.get_mut(sect_y) else { - panic!( - "section index {sect_y} out of bounds for chunk with {} sections", - self.section_count() - ) - }; - - // Mark the appropriate blocks as modified. - // No need to iterate through all the blocks if we know they're all the same. - if let PalettedContainer::Single(single) = §.block_states { - if block != *single { - sect.mark_all_blocks_as_modified(); - } - } else { - for i in 0..SECTION_BLOCK_COUNT { - if block != sect.block_states.get(i) { - sect.mark_block_as_modified(i); - } - } - } - - if block.is_air() { - sect.non_air_count = 0; - } else { - sect.non_air_count = SECTION_BLOCK_COUNT as u16; - } - - sect.block_states.fill(block); - } - - fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId { - assert!( - x < 4 && y < self.section_count() * 4 && z < 4, - "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" - ); - - self.sections[y / 4].biomes.get(x + z * 4 + y % 4 * 4 * 4) - } - - fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId { - assert!( - x < 4 && y < self.section_count() * 4 && z < 4, - "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" - ); - - let old_biome = self.sections[y / 4] - .biomes - .set(x + z * 4 + y % 4 * 4 * 4, biome); - - if biome != old_biome { - self.any_biomes_modified = true; - } - - old_biome - } - - fn fill_biomes(&mut self, sect_y: usize, biome: BiomeId) { - let Some(sect) = self.sections.get_mut(sect_y) else { - panic!( - "section index {sect_y} out of bounds for chunk with {} sections", - self.section_count() - ) - }; - - sect.biomes.fill(biome); - - // TODO: this is set to true unconditionally, but it doesn't have to be. - self.any_biomes_modified = true; - } - - fn optimize(&mut self) { - for sect in self.sections.iter_mut() { - sect.block_states.optimize(); - sect.biomes.optimize(); - } - - self.cached_init_packet.get_mut().unwrap().shrink_to_fit(); - self.cached_update_packets.shrink_to_fit(); - } -} - -fn compact_u64s_len(vals_count: usize, bits_per_val: usize) -> usize { - let vals_per_u64 = 64 / bits_per_val; - num::Integer::div_ceil(&vals_count, &vals_per_u64) -} - -#[inline] -fn encode_compact_u64s( - mut w: impl Write, - mut vals: impl Iterator, - bits_per_val: usize, -) -> anyhow::Result<()> { - debug_assert!(bits_per_val <= 64); - - let vals_per_u64 = 64 / bits_per_val; - - loop { - let mut n = 0; - for i in 0..vals_per_u64 { - match vals.next() { - Some(val) => { - debug_assert!(val < 2_u128.pow(bits_per_val as _) as _); - n |= val << (i * bits_per_val); - } - None if i > 0 => return n.encode(&mut w), - None => return Ok(()), - } - } - n.encode(&mut w)?; - } -} - -#[cfg(test)] -mod tests { - use rand::prelude::*; - - use super::*; - use crate::config::MockConfig; - - fn check_invariants(sections: &[ChunkSection]) { - for sect in sections { - assert_eq!( - (0..SECTION_BLOCK_COUNT) - .filter(|&i| !sect.block_states.get(i).is_air()) - .count(), - sect.non_air_count as usize, - "number of non-air blocks does not match counter" - ); - } - } - - fn rand_block_state(rng: &mut (impl Rng + ?Sized)) -> BlockState { - BlockState::from_raw(rng.gen_range(0..=BlockState::max_raw())).unwrap() - } - - #[test] - fn random_block_assignments() { - let mut rng = thread_rng(); - - let height = 512; - - let mut loaded = LoadedChunk::::new(UnloadedChunk::default(), height / 16, ()); - let mut unloaded = UnloadedChunk::new(height); - - for i in 0..10_000 { - let state = if i % 250 == 0 { - [BlockState::AIR, BlockState::CAVE_AIR, BlockState::VOID_AIR] - .into_iter() - .choose(&mut rng) - .unwrap() - } else { - rand_block_state(&mut rng) - }; - - let x = rng.gen_range(0..16); - let y = rng.gen_range(0..height); - let z = rng.gen_range(0..16); - - loaded.set_block_state(x, y, z, state); - unloaded.set_block_state(x, y, z, state); - } - - check_invariants(&loaded.sections); - check_invariants(&unloaded.sections); - - loaded.optimize(); - unloaded.optimize(); - - check_invariants(&loaded.sections); - check_invariants(&unloaded.sections); - - loaded.fill_block_states( - rng.gen_range(0..loaded.section_count()), - rand_block_state(&mut rng), - ); - unloaded.fill_block_states( - rng.gen_range(0..loaded.section_count()), - rand_block_state(&mut rng), - ); - - check_invariants(&loaded.sections); - check_invariants(&unloaded.sections); - } -} diff --git a/crates/valence/src/chunk/entity_partition.rs b/crates/valence/src/chunk/entity_partition.rs deleted file mode 100644 index 51574cb..0000000 --- a/crates/valence/src/chunk/entity_partition.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::collections::hash_map::Entry; -use std::collections::BTreeSet; - -use crate::chunk::ChunkPos; -use crate::config::Config; -use crate::entity::{Entities, EntityId}; -use crate::packet::PacketWriter; -use crate::world::Worlds; - -pub struct PartitionCell { - /// Entities in this cell. - /// Invariant: After [`update_entity_partition`] is called, contains only - /// valid IDs and non-deleted entities with positions inside this cell. - entities: BTreeSet, - /// Entities that have entered the chunk this tick, paired with the cell - /// position in this world they came from. - incoming: Vec<(EntityId, Option)>, - /// Entities that have left the chunk this tick, paired with the cell - /// position in this world they arrived at. - outgoing: Vec<(EntityId, Option)>, - /// A cache of packets needed to update all the `entities` in this chunk. - cached_update_packets: Vec, -} - -impl PartitionCell { - pub(super) fn new() -> Self { - Self { - entities: BTreeSet::new(), - incoming: vec![], - outgoing: vec![], - cached_update_packets: vec![], - } - } - - pub fn entities(&self) -> impl ExactSizeIterator + '_ { - self.entities.iter().cloned() - } - - pub fn incoming(&self) -> &[(EntityId, Option)] { - &self.incoming - } - - pub fn outgoing(&self) -> &[(EntityId, Option)] { - &self.outgoing - } - - pub fn cached_update_packets(&self) -> &[u8] { - &self.cached_update_packets - } - - pub(super) fn clear_incoming_outgoing(&mut self) { - self.incoming.clear(); - self.outgoing.clear(); - } -} - -/// Prepares the entity partitions in all worlds for the client update -/// procedure. -pub fn update_entity_partition( - entities: &mut Entities, - worlds: &mut Worlds, - compression_threshold: Option, -) { - for (entity_id, entity) in entities.iter() { - let pos = ChunkPos::at(entity.position().x, entity.position().z); - let old_pos = ChunkPos::at(entity.old_position().x, entity.old_position().z); - - let world = entity.world(); - let old_world = entity.old_world(); - - if entity.deleted() { - // Entity was deleted. Remove it from the chunk it was in, if it was in a chunk - // at all. - if let Some(old_world) = worlds.get_mut(old_world) { - if let Some(old_cell) = old_world.chunks.cell_mut(old_pos) { - if old_cell.entities.remove(&entity_id) { - old_cell.outgoing.push((entity_id, None)); - } - } - } - } else if old_world != world { - // TODO: skip marker entity. - - // Entity changed the world it is in. Remove it from old chunk and - // insert it in the new chunk. - if let Some(old_world) = worlds.get_mut(old_world) { - if let Some(old_cell) = old_world.chunks.cell_mut(old_pos) { - if old_cell.entities.remove(&entity_id) { - old_cell.outgoing.push((entity_id, None)); - } - } - } - - if let Some(world) = worlds.get_mut(world) { - match world.chunks.chunks.entry(pos) { - Entry::Occupied(oe) => { - let cell = &mut oe.into_mut().1; - if cell.entities.insert(entity_id) { - cell.incoming.push((entity_id, None)); - } - } - Entry::Vacant(ve) => { - let cell = PartitionCell { - entities: BTreeSet::from([entity_id]), - incoming: vec![(entity_id, None)], - outgoing: vec![], - cached_update_packets: vec![], - }; - - ve.insert((None, cell)); - } - } - } - } else if pos != old_pos { - // TODO: skip marker entity. - - // Entity changed its chunk position without changing worlds. Remove - // it from old chunk and insert it in new chunk. - if let Some(world) = worlds.get_mut(world) { - if let Some(old_cell) = world.chunks.cell_mut(old_pos) { - if old_cell.entities.remove(&entity_id) { - old_cell.outgoing.push((entity_id, Some(pos))); - } - } - - match world.chunks.chunks.entry(pos) { - Entry::Occupied(oe) => { - let cell = &mut oe.into_mut().1; - if cell.entities.insert(entity_id) { - cell.incoming.push((entity_id, Some(old_pos))); - } - } - Entry::Vacant(ve) => { - let cell = PartitionCell { - entities: BTreeSet::from([entity_id]), - incoming: vec![(entity_id, Some(old_pos))], - outgoing: vec![], - cached_update_packets: vec![], - }; - - ve.insert((None, cell)); - } - } - } - } else { - // The entity didn't change its chunk position so there is nothing - // we need to do. - } - } - - // Cache the entity update packets. - let mut scratch = vec![]; - let mut compression_scratch = vec![]; - - for (_, world) in worlds.iter_mut() { - for cell in world.chunks.cells_mut() { - cell.cached_update_packets.clear(); - - for &id in &cell.entities { - let start = cell.cached_update_packets.len(); - - let writer = PacketWriter::new( - &mut cell.cached_update_packets, - compression_threshold, - &mut compression_scratch, - ); - - let entity = &mut entities[id]; - - entity - .write_update_packets(writer, id, &mut scratch) - .unwrap(); - - let end = cell.cached_update_packets.len(); - entity.self_update_range = start..end; - } - } - } -} diff --git a/crates/valence/src/chunk/pos.rs b/crates/valence/src/chunk/pos.rs deleted file mode 100644 index 95ff09b..0000000 --- a/crates/valence/src/chunk/pos.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::iter::FusedIterator; - -use valence_protocol::BlockPos; - -/// The X and Z position of a chunk in a world. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Debug)] -pub struct ChunkPos { - /// The X position of the chunk. - pub x: i32, - /// The Z position of the chunk. - pub z: i32, -} - -const EXTRA_VIEW_RADIUS: i32 = 2; - -impl ChunkPos { - /// Constructs a new chunk position. - pub const fn new(x: i32, z: i32) -> Self { - Self { x, z } - } - - /// Takes an X and Z position in world space and returns the chunk position - /// containing the point. - pub fn at(x: f64, z: f64) -> Self { - Self::new((x / 16.0).floor() as i32, (z / 16.0).floor() as i32) - } - - /// Checks if two chunk positions are within a view distance (render - /// distance) of each other such that a client standing in `self` would - /// be able to see `other`. - #[inline] - pub fn is_in_view(self, other: Self, view_dist: u8) -> bool { - let dist = view_dist as i64 + EXTRA_VIEW_RADIUS as i64; - - let diff_x = other.x as i64 - self.x as i64; - let diff_z = other.z as i64 - self.z as i64; - - diff_x * diff_x + diff_z * diff_z <= dist * dist - } - - /// Returns an iterator over all chunk positions within a view distance - /// centered on `self`. The `self` position is included in the output. - pub fn in_view(self, view_dist: u8) -> impl FusedIterator { - let dist = view_dist as i32 + EXTRA_VIEW_RADIUS; - - (self.z - dist..=self.z + dist) - .flat_map(move |z| (self.x - dist..=self.x + dist).map(move |x| Self { x, z })) - .filter(move |&p| self.is_in_view(p, view_dist)) - } - - // `in_view` wasn't optimizing well so we're using this for now. - #[inline(always)] - pub(crate) fn try_for_each_in_view(self, view_dist: u8, mut f: F) -> anyhow::Result<()> - where - F: FnMut(ChunkPos) -> anyhow::Result<()>, - { - let dist = view_dist as i32 + EXTRA_VIEW_RADIUS; - - for z in self.z - dist..=self.z + dist { - for x in self.x - dist..=self.x + dist { - let p = Self { x, z }; - if self.is_in_view(p, view_dist) { - f(p)?; - } - } - } - - Ok(()) - } -} - -impl From<(i32, i32)> for ChunkPos { - fn from((x, z): (i32, i32)) -> Self { - Self { x, z } - } -} - -impl From for (i32, i32) { - fn from(pos: ChunkPos) -> Self { - (pos.x, pos.z) - } -} - -impl From<[i32; 2]> for ChunkPos { - fn from([x, z]: [i32; 2]) -> Self { - Self { x, z } - } -} - -impl From for [i32; 2] { - fn from(pos: ChunkPos) -> Self { - [pos.x, pos.z] - } -} - -impl From for ChunkPos { - fn from(pos: BlockPos) -> Self { - Self::new(pos.x.div_euclid(16), pos.z.div_euclid(16)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn all_in_view() { - let center = ChunkPos::new(42, 24); - - for dist in 2..=32 { - for pos in center.in_view(dist) { - assert!(center.is_in_view(pos, dist)); - assert!(pos.is_in_view(center, dist)); - } - } - } -} diff --git a/crates/valence/src/client.rs b/crates/valence/src/client.rs index c5f2f5c..9a1909d 100644 --- a/crates/valence/src/client.rs +++ b/crates/valence/src/client.rs @@ -1,367 +1,182 @@ -//! Connections to the server after logging in. - -use std::iter::FusedIterator; use std::net::IpAddr; use std::num::Wrapping; -use std::ops::{Deref, DerefMut}; -use std::{array, fmt, mem}; -use anyhow::{bail, ensure, Context}; -pub use bitfield_struct::bitfield; -pub use event::ClientEvent; -use rayon::iter::ParallelIterator; -use tokio::sync::OwnedSemaphorePermit; -use tracing::{info, warn}; +use anyhow::{bail, Context}; +use bevy_ecs::prelude::*; +use bytes::BytesMut; +use glam::{DVec3, Vec3}; +use tracing::warn; use uuid::Uuid; -use valence_protocol::packets::s2c::particle::{Particle, ParticleS2c}; +use valence_protocol::packets::s2c::particle::Particle; use valence_protocol::packets::s2c::play::{ - AcknowledgeBlockChange, ClearTitles, CloseContainerS2c, CombatDeath, DisconnectPlay, - EntityAnimationS2c, EntityEvent, GameEvent, KeepAliveS2c, LoginPlayOwned, OpenScreen, - PluginMessageS2c, RemoveEntitiesEncode, ResourcePackS2c, RespawnOwned, SetActionBarText, - SetCenterChunk, SetContainerContentEncode, SetContainerSlotEncode, SetDefaultSpawnPosition, - SetEntityMetadata, SetEntityVelocity, SetExperience, SetHealth, SetRenderDistance, - SetSubtitleText, SetTitleAnimationTimes, SetTitleText, SynchronizePlayerPosition, - SystemChatMessage, UnloadChunk, UpdateAttributes, UpdateTime, -}; -use valence_protocol::types::{ - AttributeProperty, DisplayedSkinParts, GameEventKind, GameMode, SyncPlayerPosLookFlags, + AcknowledgeBlockChange, CombatDeath, DisconnectPlay, EntityEvent, GameEvent, KeepAliveS2c, + LoginPlayOwned, ParticleS2c, PluginMessageS2c, RemoveEntitiesEncode, ResourcePackS2c, + RespawnOwned, SetActionBarText, SetCenterChunk, SetDefaultSpawnPosition, SetEntityMetadata, + SetEntityVelocity, SetRenderDistance, SetSubtitleText, SetTitleAnimationTimes, SetTitleText, + SynchronizePlayerPosition, SystemChatMessage, UnloadChunk, }; +use valence_protocol::types::{GameEventKind, GameMode, Property, SyncPlayerPosLookFlags}; use valence_protocol::{ - BlockPos, EncodePacket, Ident, ItemStack, RawBytes, Text, Username, VarInt, + BlockPos, EncodePacket, Ident, ItemStack, PacketDecoder, PacketEncoder, RawBytes, Text, + Username, VarInt, }; -use vek::Vec3; -use crate::chunk::ChunkPos; -use crate::client::event::next_event_fallible; -use crate::config::Config; use crate::dimension::DimensionId; use crate::entity::data::Player; -use crate::entity::{self, velocity_to_packet_units, Entities, EntityId, StatusOrAnimation}; -use crate::inventory::{Inventories, InventoryId}; -use crate::player_list::{PlayerListId, PlayerLists}; -use crate::player_textures::SignedPlayerTextures; -use crate::server::{NewClientData, PlayPacketReceiver, PlayPacketSender, SharedServer}; -use crate::slab_versioned::{Key, VersionedSlab}; -use crate::world::{WorldId, Worlds}; -use crate::Ticks; +use crate::entity::{velocity_to_packet_units, EntityStatus, McEntity}; +use crate::instance::Instance; +use crate::packet::WritePacket; +use crate::server::{NewClientInfo, Server}; +use crate::view::{ChunkPos, ChunkView}; +use crate::{Despawned, NULL_ENTITY}; -mod event; +pub mod event; -/// A container for all [`Client`]s on a [`Server`](crate::server::Server). -/// -/// New clients are automatically inserted into this container but -/// are not automatically deleted. It is your responsibility to delete them once -/// they disconnect. This can be checked with [`Client::is_disconnected`]. -pub struct Clients { - slab: VersionedSlab>, -} - -impl Clients { - pub(crate) fn new() -> Self { - Self { - slab: VersionedSlab::new(), - } - } - - pub(crate) fn insert(&mut self, client: Client) -> (ClientId, &mut Client) { - let (k, client) = self.slab.insert(client); - (ClientId(k), client) - } - - /// Removes a client from the server. - /// - /// If the given client ID is valid, the client's `ClientState` is returned - /// and the client is deleted. Otherwise, `None` is returned and the - /// function has no effect. - pub fn remove(&mut self, client: ClientId) -> Option { - self.slab.remove(client.0).map(|c| { - info!(username = %c.username, uuid = %c.uuid, ip = %c.ip, "removing client"); - c.state - }) - } - - /// Deletes all clients from the server for which `f` returns `false`. - /// - /// All clients are visited in an unspecified order. - pub fn retain(&mut self, mut f: impl FnMut(ClientId, &mut Client) -> bool) { - self.slab.retain(|k, v| { - if !f(ClientId(k), v) { - info!(username = %v.username, uuid = %v.uuid, ip = %v.ip, "removing client"); - false - } else { - true - } - }) - } - - /// Returns the number of clients on the server. This includes clients for - /// which [`Client::is_disconnected`] returns true. - pub fn len(&self) -> usize { - self.slab.len() - } - - /// Returns `true` if there are no clients on the server. This includes - /// clients for which [`Client::is_disconnected`] returns true. - pub fn is_empty(&self) -> bool { - self.slab.len() == 0 - } - - /// Returns a shared reference to the client with the given ID. If - /// the ID is invalid, then `None` is returned. - pub fn get(&self, client: ClientId) -> Option<&Client> { - self.slab.get(client.0) - } - - /// Returns an exclusive reference to the client with the given ID. If the - /// ID is invalid, then `None` is returned. - pub fn get_mut(&mut self, client: ClientId) -> Option<&mut Client> { - self.slab.get_mut(client.0) - } - - /// Returns an iterator over all clients on the server in an unspecified - /// order. - pub fn iter( - &self, - ) -> impl ExactSizeIterator)> + FusedIterator + Clone + '_ { - self.slab.iter().map(|(k, v)| (ClientId(k), v)) - } - - /// Returns a mutable iterator over all clients on the server in an - /// unspecified order. - pub fn iter_mut( - &mut self, - ) -> impl ExactSizeIterator)> + FusedIterator + '_ { - self.slab.iter_mut().map(|(k, v)| (ClientId(k), v)) - } - - /// Returns a parallel iterator over all clients on the server in an - /// unspecified order. - pub fn par_iter(&self) -> impl ParallelIterator)> + Clone + '_ { - self.slab.par_iter().map(|(k, v)| (ClientId(k), v)) - } - - /// Returns a parallel mutable iterator over all clients on the server in an - /// unspecified order. - pub fn par_iter_mut( - &mut self, - ) -> impl ParallelIterator)> + '_ { - self.slab.par_iter_mut().map(|(k, v)| (ClientId(k), v)) - } -} - -/// An identifier for a [`Client`] on the server. -/// -/// Client IDs are either _valid_ or _invalid_. Valid client IDs point to -/// clients that have not been deleted, while invalid IDs point to those that -/// have. Once an ID becomes invalid, it will never become valid again. -/// -/// The [`Ord`] instance on this type is correct but otherwise unspecified. This -/// is useful for storing IDs in containers such as -/// [`BTreeMap`](std::collections::BTreeMap). -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub struct ClientId(Key); - -impl ClientId { - /// The value of the default client ID which is always invalid. - pub const NULL: Self = Self(Key::NULL); -} - -/// Represents a remote connection to a client after successfully logging in. -/// -/// Much like an [`Entity`], clients possess a location, rotation, and UUID. -/// However, clients are handled separately from entities and are partially -/// managed by the library. -/// -/// By default, clients have no influence over the worlds they reside in. They -/// cannot break blocks, hurt entities, or see other clients. Interactions with -/// the server must be handled explicitly with [`Self::next_event`]. -/// -/// Additionally, clients possess [`Player`] entity data which is only visible -/// to themselves. This can be accessed with [`Self::player`] and -/// [`Self::player_mut`]. -/// -/// # The Difference Between a "Client" and a "Player" -/// -/// Normally in Minecraft, players and clients are one and the same. Players are -/// simply a subtype of the entity base class backed by a remote connection. -/// -/// In Valence however, clients and players are decoupled. This separation -/// allows for greater flexibility and enables parallelism. -/// -/// [`Entity`]: crate::entity::Entity -pub struct Client { - /// Custom state. - pub state: C::ClientState, - send: Option, - recv: PlayPacketReceiver, - /// To make sure we're not loading already loaded chunks, or unloading - /// unloaded chunks. - #[cfg(debug_assertions)] - loaded_chunks: std::collections::HashSet, - /// Ensures that we don't allow more connections to the server until the - /// client is dropped. - _permit: OwnedSemaphorePermit, - /// General purpose reusable buffer. +/// Represents a client connected to the server. Used to send and receive +/// packets from the client. +#[derive(Component)] +pub struct Client { + conn: Box, + enc: PacketEncoder, + dec: PacketDecoder, scratch: Vec, - /// Reused buffer for unloading entities. - entities_to_unload: Vec, - /// The entity with the same UUID as this client. - self_entity: EntityId, + is_disconnected: bool, username: Username, uuid: Uuid, ip: IpAddr, - textures: Option, - /// World client is currently in. Default value is **invalid** and must - /// be set by calling [`Client::respawn`]. - world: WorldId, - old_world: WorldId, - player_list: Option, - /// Player list from the previous tick. - old_player_list: Option, - position: Vec3, - /// Position from the previous tick. - old_position: Vec3, - /// Measured in degrees + properties: Vec, + instance: Entity, + old_instance: Entity, + position: DVec3, + old_position: DVec3, + position_modified: bool, yaw: f32, - /// Measured in degrees + yaw_modified: bool, pitch: f32, + pitch_modified: bool, + on_ground: bool, + game_mode: GameMode, + op_level: u8, + block_change_sequence: i32, + // TODO: make this a component and default to the self-entity's player data? + player_data: Player, view_distance: u8, old_view_distance: u8, + death_location: Option<(DimensionId, BlockPos)>, + entities_to_despawn: Vec, + got_keepalive: bool, + last_keepalive_id: u64, /// Counts up as teleports are made. teleport_id_counter: u32, /// The number of pending client teleports that have yet to receive a /// confirmation. Inbound client position packets should be ignored while /// this is nonzero. pending_teleports: u32, - death_location: Option<(DimensionId, BlockPos)>, - /// The ID of the last keepalive sent. - last_keepalive_id: u64, - game_mode: GameMode, - block_change_sequence: i32, - /// The data for the client's own player entity. - player_data: Player, - /// The client's inventory slots. - slots: Box<[Option; 45]>, - /// Contains a set bit for each modified slot in `slots` made by the server - /// this tick. - modified_slots: u64, - /// Counts up as inventory modifications are made by the server. Used to - /// prevent desync. - inv_state_id: Wrapping, - /// The item currently held by the client's cursor in the inventory. - cursor_item: Option, - /// The currently open inventory. The client can close the screen, making - /// this [`Option::None`]. - open_inventory: Option, + /// If the client needs initialization. + is_new: bool, + /// If the client needs to be sent the respawn packet for the current world. + needs_respawn: bool, + is_hardcore: bool, + is_flat: bool, + has_respawn_screen: bool, + /// The item that the client thinks it's holding under the mouse + /// cursor. + pub(crate) cursor_item: Option, + pub(crate) cursor_item_modified: bool, /// The current window ID. Incremented when inventories are opened. - window_id: u8, - bits: ClientBits, + pub(crate) window_id: u8, + pub(crate) inventory_state_id: Wrapping, + /// Tracks what slots have been modified by this client in this tick, so we + /// don't need to send updates for them. + pub(crate) inventory_slots_modified: u64, + pub(crate) held_item_slot: u16, } -#[bitfield(u8)] -struct ClientBits { - created_this_tick: bool, - respawn: bool, - /// If the last sent keepalive got a response. - got_keepalive: bool, - hardcore: bool, - flat: bool, - respawn_screen: bool, - cursor_item_modified: bool, - open_inventory_modified: bool, - //#[bits(1)] - //_pad: u8, +pub trait ClientConnection: Send + Sync + 'static { + fn try_send(&mut self, bytes: BytesMut) -> anyhow::Result<()>; + fn try_recv(&mut self) -> anyhow::Result; } -impl Deref for Client { - type Target = C::ClientState; - - fn deref(&self) -> &Self::Target { - &self.state - } -} - -impl DerefMut for Client { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.state - } -} - -impl Client { +impl Client { pub(crate) fn new( - send: PlayPacketSender, - recv: PlayPacketReceiver, - permit: OwnedSemaphorePermit, - ncd: NewClientData, - state: C::ClientState, + info: NewClientInfo, + conn: Box, + enc: PacketEncoder, + dec: PacketDecoder, ) -> Self { Self { - state, - send: Some(send), - recv, - #[cfg(debug_assertions)] - loaded_chunks: Default::default(), - _permit: permit, + conn, + enc, + dec, scratch: vec![], - entities_to_unload: vec![], - self_entity: EntityId::NULL, - username: ncd.username, - uuid: ncd.uuid, - ip: ncd.ip, - textures: ncd.textures, - world: WorldId::NULL, - old_world: WorldId::NULL, - player_list: None, - old_player_list: None, - position: Vec3::default(), - old_position: Vec3::default(), + is_disconnected: false, + username: info.username, + uuid: info.uuid, + ip: info.ip, + properties: info.properties, + instance: NULL_ENTITY, + old_instance: NULL_ENTITY, + position: DVec3::ZERO, + old_position: DVec3::ZERO, + position_modified: true, yaw: 0.0, + yaw_modified: true, pitch: 0.0, - view_distance: 2, - old_view_distance: 2, - teleport_id_counter: 0, - pending_teleports: 0, - death_location: None, - last_keepalive_id: 0, - game_mode: GameMode::Survival, + pitch_modified: true, + on_ground: false, + game_mode: GameMode::default(), + op_level: 0, block_change_sequence: 0, player_data: Player::new(), - slots: Box::new(array::from_fn(|_| None)), - modified_slots: 0, - inv_state_id: Wrapping(0), + view_distance: 2, + old_view_distance: 2, + death_location: None, + entities_to_despawn: vec![], + is_new: true, + needs_respawn: false, + is_hardcore: false, + is_flat: false, + has_respawn_screen: false, + got_keepalive: true, + last_keepalive_id: 0, + teleport_id_counter: 0, + pending_teleports: 0, cursor_item: None, - open_inventory: None, + cursor_item_modified: false, window_id: 0, - bits: ClientBits::new() - .with_got_keepalive(true) - .with_created_this_tick(true), + inventory_state_id: Wrapping(0), + inventory_slots_modified: 0, + held_item_slot: 0, } } - /// Attempts to enqueue a play packet to be sent to this client. + pub(crate) fn is_new(&self) -> bool { + self.is_new + } + + /// Attempts to write a play packet into this client's packet buffer. The + /// packet will be sent at the end of the tick. /// /// If encoding the packet fails, the client is disconnected. Has no /// effect if the client is already disconnected. - pub fn queue_packet

(&mut self, pkt: &P) + pub fn write_packet

(&mut self, pkt: &P) where - P: EncodePacket + fmt::Debug + ?Sized, + P: EncodePacket + ?Sized, { - if let Some(send) = &mut self.send { - if let Err(e) = send.append_packet(pkt) { - warn!( - username = %self.username, - uuid = %self.uuid, - ip = %self.ip, - "failed to queue packet: {e:#}" - ); - self.send = None; - } - } + self.enc.write_packet(pkt); } - /// If the client joined the game this tick. - pub fn created_this_tick(&self) -> bool { - self.bits.created_this_tick() + /// Writes arbitrary bytes to this client's packet buffer. The packet data + /// must be properly compressed for the current compression threshold but + /// never encrypted. + /// + /// Don't use this function unless you know what you're doing. Consider + /// using [`write_packet`] instead. + /// + /// [`write_packet`]: Self::write_packet + #[inline] + pub fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.enc.append_bytes(bytes); } /// Gets the username of this client. @@ -379,346 +194,127 @@ impl Client { self.ip } - /// Gets the player textures of this client. If the client does not have - /// a skin, then `None` is returned. - pub fn textures(&self) -> Option<&SignedPlayerTextures> { - self.textures.as_ref() + /// Gets the properties from this client's game profile. + pub fn properties(&self) -> &[Property] { + &self.properties } - /// Gets the world this client is located in. - pub fn world(&self) -> WorldId { - self.world - } - - /// Gets the player list this client sees. - pub fn player_list(&self) -> Option<&PlayerListId> { - self.player_list.as_ref() - } - - /// Sets the player list this client sees. + /// Gets whether or not the client is connected to the server. /// - /// The previous player list ID is returned. - pub fn set_player_list(&mut self, id: impl Into>) -> Option { - mem::replace(&mut self.player_list, id.into()) + /// A disconnected client component will never become reconnected. It is + /// your responsibility to despawn disconnected client entities, since + /// they will not be automatically despawned by Valence. + pub fn is_disconnected(&self) -> bool { + self.is_disconnected } - /// Sets if this client sees the world as superflat. Superflat worlds have - /// a horizon line lower than normal worlds. + /// Gets the [`Instance`] entity this client is located in. The client is + /// not in any instance when they first join. + pub fn instance(&self) -> Entity { + self.instance + } + + /// Sets the [`Instance`] entity this client is located in. This can be used + /// to respawn the client after death. /// - /// The player must be (re)spawned for changes to take effect. - pub fn set_flat(&mut self, flat: bool) { - self.bits.set_flat(flat); + /// The given [`Entity`] must exist and have the [`Instance`] component. + /// Otherwise, the client is disconnected at the end of the tick. + pub fn set_instance(&mut self, instance: Entity) { + self.instance = instance; + self.needs_respawn = true; } - /// Gets if this client sees the world as superflat. Superflat worlds have - /// a horizon line lower than normal worlds. - pub fn is_flat(&self) -> bool { - self.bits.flat() - } - - /// Changes the world this client is located in and respawns the client. - /// This can be used to respawn the client after death. - /// - /// The given [`WorldId`] must be valid. Otherwise, the client is - /// disconnected. - pub fn respawn(&mut self, world: WorldId) { - self.world = world; - self.bits.set_respawn(true); - } - - /// Sends a system message to the player which is visible in the chat. The - /// message is only visible to this client. - pub fn send_message(&mut self, msg: impl Into) { - self.queue_packet(&SystemChatMessage { - chat: msg.into(), - overlay: false, - }); - } - - pub fn send_plugin_message(&mut self, channel: Ident<&str>, data: &[u8]) { - self.queue_packet(&PluginMessageS2c { - channel, - data: RawBytes(data), - }); - } - - /// Gets the absolute position of this client in the world it is located + /// Gets the absolute position of this client in the instance it is located /// in. - pub fn position(&self) -> Vec3 { + pub fn position(&self) -> DVec3 { self.position } - /// Changes the position and rotation of this client in the world it is - /// located in. - /// - /// If you want to change the client's world, use [`Self::respawn`]. - pub fn teleport(&mut self, pos: impl Into>, yaw: f32, pitch: f32) { + pub fn set_position(&mut self, pos: impl Into) { self.position = pos.into(); - self.yaw = yaw; - self.pitch = pitch; - - self.queue_packet(&SynchronizePlayerPosition { - position: self.position.into_array(), - yaw, - pitch, - flags: SyncPlayerPosLookFlags::new(), - teleport_id: VarInt(self.teleport_id_counter as i32), - dismount_vehicle: false, - }); - - self.pending_teleports = self.pending_teleports.wrapping_add(1); - self.teleport_id_counter = self.teleport_id_counter.wrapping_add(1); + self.position_modified = true; } - /// Sets the client's velocity in m/s. - pub fn set_velocity(&mut self, velocity: impl Into>) { - self.queue_packet(&SetEntityVelocity { + /// Returns the position this client was in at the end of the previous tick. + pub fn old_position(&self) -> DVec3 { + self.old_position + } + + /// Gets a [`ChunkView`] representing the chunks this client can see. + pub fn view(&self) -> ChunkView { + ChunkView::new(ChunkPos::from_dvec3(self.position), self.view_distance) + } + + pub fn old_view(&self) -> ChunkView { + ChunkView::new( + ChunkPos::from_dvec3(self.old_position), + self.old_view_distance, + ) + } + + pub fn set_velocity(&mut self, velocity: impl Into) { + self.enc.write_packet(&SetEntityVelocity { entity_id: VarInt(0), - velocity: velocity_to_packet_units(velocity.into()).into_array(), - }) + velocity: velocity_to_packet_units(velocity.into()), + }); } - /// Gets this client's yaw. + /// Gets this client's yaw (in degrees). pub fn yaw(&self) -> f32 { self.yaw } - /// Gets this client's pitch. + /// Sets this client's yaw (in degrees). + pub fn set_yaw(&mut self, yaw: f32) { + self.yaw = yaw; + self.yaw_modified = true; + } + + /// Gets this client's pitch (in degrees). pub fn pitch(&self) -> f32 { self.pitch } - /// Sets the spawn position. The client will see `minecraft:compass` items - /// point at the provided position. - pub fn set_spawn_position(&mut self, pos: impl Into, yaw_degrees: f32) { - self.queue_packet(&SetDefaultSpawnPosition { - position: pos.into(), - angle: yaw_degrees, - }); + /// Sets this client's pitch (in degrees). + pub fn set_pitch(&mut self, pitch: f32) { + self.pitch = pitch; + self.pitch_modified = true; } - /// Gets the last death location of this client. The client will see - /// `minecraft:recovery_compass` items point at the returned position. - /// - /// If the client's current dimension differs from the returned - /// dimension or the location is `None` then the compass will spin - /// randomly. - pub fn death_location(&self) -> Option<(DimensionId, BlockPos)> { - self.death_location - } - - /// Sets the last death location. The client will see - /// `minecraft:recovery_compass` items point at the provided position. - /// If the client's current dimension differs from the provided - /// dimension or the location is `None` then the compass will spin - /// randomly. - /// - /// Changes to the last death location take effect when the client - /// (re)spawns. - pub fn set_death_location(&mut self, location: Option<(DimensionId, BlockPos)>) { - self.death_location = location; - } - - /// Gets the client's game mode. - pub fn game_mode(&self) -> GameMode { - self.game_mode - } - - /// Sets the client's game mode. - pub fn set_game_mode(&mut self, game_mode: GameMode) { - if self.game_mode != game_mode { - self.game_mode = game_mode; - - if !self.created_this_tick() { - self.queue_packet(&GameEvent { - kind: GameEventKind::ChangeGameMode, - value: game_mode as i32 as f32, - }); - } - } - } - - /// Sets whether or not the client sees rain. - pub fn set_raining(&mut self, raining: bool) { - self.queue_packet(&GameEvent { - kind: if raining { - GameEventKind::BeginRaining - } else { - GameEventKind::EndRaining - }, - value: 0.0, - }); - } - - /// Sets the client's rain level. This changes the sky color and lightning - /// on the client. - /// - /// The rain level is clamped between `0.0.` and `1.0`. - pub fn set_rain_level(&mut self, rain_level: f32) { - self.queue_packet(&GameEvent { - kind: GameEventKind::RainLevelChange, - value: rain_level.clamp(0.0, 1.0), - }); - } - - /// Sets the client's thunder level. This changes the sky color and - /// lightning on the client. - /// - /// For this to take effect, it must already be raining via - /// [`set_raining`](Self::set_raining) or - /// [`set_rain_level`](Self::set_rain_level). - /// - /// The thunder level is clamped between `0.0` and `1.0`. - pub fn set_thunder_level(&mut self, thunder_level: f32) { - self.queue_packet(&GameEvent { - kind: GameEventKind::ThunderLevelChange, - value: thunder_level.clamp(0.0, 1.0), - }); - } - - pub fn play_particle( - &mut self, - particle: &Particle, - long_distance: bool, - position: impl Into>, - offset: impl Into>, - max_speed: f32, - count: i32, - ) { - self.queue_packet(&ParticleS2c { - particle: particle.clone(), - long_distance, - position: position.into().into_array(), - offset: offset.into().into_array(), - max_speed, - count, - }) - } - - /// Sets the title this client sees. - /// - /// A title is a large piece of text displayed in the center of the screen - /// which may also include a subtitle underneath it. The title can be - /// configured to fade in and out using the [`SetTitleAnimationTimes`] - /// struct. - pub fn set_title( - &mut self, - title: impl Into, - subtitle: impl Into, - animation: impl Into>, - ) { - let title = title.into(); - let subtitle = subtitle.into(); - - self.queue_packet(&SetTitleText { title_text: title }); - - if !subtitle.is_empty() { - self.queue_packet(&SetSubtitleText { - subtitle_text: subtitle, - }); - } - - if let Some(anim) = animation.into() { - self.queue_packet(&anim); - } - } - - /// Sets the action bar for this client. - pub fn set_action_bar(&mut self, text: impl Into) { - self.queue_packet(&SetActionBarText { - action_bar_text: text.into(), - }); - } - - /// Sets the attack cooldown speed. - pub fn set_attack_speed(&mut self, speed: f64) { - self.queue_packet(&UpdateAttributes { - entity_id: VarInt(0), - properties: vec![AttributeProperty { - key: Ident::new("generic.attack_speed").unwrap(), - value: speed, - modifiers: vec![], - }], - }); - } - - /// Sets the speed at which the client can run on the ground. - pub fn set_movement_speed(&mut self, speed: f64) { - self.queue_packet(&UpdateAttributes { - entity_id: VarInt(0), - properties: vec![AttributeProperty { - key: Ident::new("generic.movement_speed").unwrap(), - value: speed, - modifiers: vec![], - }], - }); - } - - /// Removes the current title from the client's screen. - pub fn clear_title(&mut self) { - self.queue_packet(&ClearTitles { reset: true }); - } - - /// Sets the XP bar visible above hotbar and total experience. - /// - /// # Arguments - /// * `bar` - Floating value in the range `0.0..=1.0` indicating progress on - /// the XP bar. - /// * `level` - Number above the XP bar. - /// * `total_xp` - TODO. - pub fn set_level(&mut self, bar: f32, level: i32, total_xp: i32) { - self.queue_packet(&SetExperience { - bar, - level: level.into(), - total_xp: total_xp.into(), - }) - } - - /// Sets the health and food of the player. - /// You can read more about hunger and saturation [here](https://minecraft.fandom.com/wiki/Food#Hunger_vs._Saturation). - /// - /// # Arguments - /// * `health` - Float in range `0.0..=20.0`. Value `<=0` is legal and will - /// kill the player. - /// * `food` - Integer in range `0..=20`. - /// * `food_saturation` - Float in range `0.0..=5.0`. - pub fn set_health_and_food(&mut self, health: f32, food: i32, food_saturation: f32) { - self.queue_packet(&SetHealth { - health, - food: food.into(), - food_saturation, - }) + /// Whether or not the client reports that it is currently on the ground. + pub fn on_ground(&self) -> bool { + self.on_ground } /// Kills the client and shows `message` on the death screen. If an entity - /// killed the player, pass its ID into the function. - pub fn kill(&mut self, killer: Option, message: impl Into) { - self.queue_packet(&CombatDeath { + /// killed the player, you should supply it as `killer`. + pub fn kill(&mut self, killer: Option<&McEntity>, message: impl Into) { + self.write_packet(&CombatDeath { player_id: VarInt(0), - entity_id: killer.map_or(-1, |k| k.to_raw()), - message: message.into(), + entity_id: killer.map_or(-1, |k| k.protocol_id()), + message: message.into().into(), }); } /// Respawns client. Optionally can roll the credits before respawning. pub fn win_game(&mut self, show_credits: bool) { - self.queue_packet(&GameEvent { + self.write_packet(&GameEvent { kind: GameEventKind::WinGame, value: if show_credits { 1.0 } else { 0.0 }, }); } pub fn has_respawn_screen(&self) -> bool { - self.bits.respawn_screen() + self.has_respawn_screen } /// Sets whether respawn screen should be displayed after client's death. pub fn set_respawn_screen(&mut self, enable: bool) { - if self.bits.respawn_screen() != enable { - self.bits.set_respawn_screen(enable); + if self.has_respawn_screen != enable { + self.has_respawn_screen = enable; - if !self.created_this_tick() { - self.queue_packet(&GameEvent { + if !self.is_new { + self.write_packet(&GameEvent { kind: GameEventKind::EnableRespawnScreen, value: if enable { 0.0 } else { 1.0 }, }); @@ -726,49 +322,18 @@ impl Client { } } - pub fn skin_parts(&self) -> DisplayedSkinParts { - DisplayedSkinParts::new() - .with_cape(self.player_data.get_cape()) - .with_jacket(self.player_data.get_jacket()) - .with_left_sleeve(self.player_data.get_left_sleeve()) - .with_right_sleeve(self.player_data.get_right_sleeve()) - .with_left_pants_leg(self.player_data.get_left_pants_leg()) - .with_right_pants_leg(self.player_data.get_right_pants_leg()) - .with_hat(self.player_data.get_hat()) - } - - pub fn set_skin_parts(&mut self, parts: DisplayedSkinParts) { - self.player_data.set_cape(parts.cape()); - self.player_data.set_jacket(parts.jacket()); - self.player_data.set_left_sleeve(parts.left_sleeve()); - self.player_data.set_right_sleeve(parts.right_sleeve()); - self.player_data.set_left_pants_leg(parts.left_pants_leg()); - self.player_data - .set_right_pants_leg(parts.right_pants_leg()); - self.player_data.set_hat(parts.hat()); - } - - /// Gets whether or not the client is connected to the server. + /// Gets whether or not the client thinks it's on a superflat world. /// - /// A disconnected client object will never become reconnected. It is your - /// responsibility to remove disconnected clients from the [`Clients`] - /// container. - pub fn is_disconnected(&self) -> bool { - self.send.is_none() + /// Modifies how the skybox is rendered. + pub fn is_flat(&self) -> bool { + self.is_flat } - /// Sends an entity event for the client's own player data. - pub fn send_entity_event(&mut self, event: entity::EntityEvent) { - match event.status_or_animation() { - StatusOrAnimation::Status(code) => self.queue_packet(&EntityEvent { - entity_id: 0, - entity_status: code, - }), - StatusOrAnimation::Animation(code) => self.queue_packet(&EntityAnimationS2c { - entity_id: VarInt(0), - animation: code, - }), - } + /// Sets whether or not the client thinks it's on a superflat world. + /// + /// Modifies how the skybox is rendered. + pub fn set_flat(&mut self, flat: bool) { + self.is_flat = flat; } /// The current view distance of this client measured in chunks. The client @@ -787,17 +352,124 @@ impl Client { self.view_distance = dist.clamp(2, 32); } - /// Enables hardcore mode. This changes the design of the client's hearts. + /// Gets the last death location of this client. The client will see + /// `minecraft:recovery_compass` items point at the returned position. /// - /// To have any visible effect, this function must be called on the same - /// tick the client joins the server. - pub fn set_hardcore(&mut self, hardcore: bool) { - self.bits.set_hardcore(hardcore); + /// If the client's current dimension differs from the returned + /// dimension or the location is `None` then the compass will spin + /// randomly. + pub fn death_location(&self) -> Option<(DimensionId, BlockPos)> { + self.death_location } - /// Gets if hardcore mode is enabled. - pub fn is_hardcore(&self) -> bool { - self.bits.hardcore() + /// Gets the client's game mode. + pub fn game_mode(&self) -> GameMode { + self.game_mode + } + + /// Sets the client's game mode. + pub fn set_game_mode(&mut self, game_mode: GameMode) { + if self.game_mode != game_mode { + self.game_mode = game_mode; + + if !self.is_new { + self.write_packet(&GameEvent { + kind: GameEventKind::ChangeGameMode, + value: game_mode as i32 as f32, + }); + } + } + } + + /// Sets the client's OP level. + pub fn set_op_level(&mut self, op_level: u8) { + self.op_level = op_level; + + if op_level > 4 { + return; + } + + self.write_packet(&EntityEvent { + entity_id: 0, + entity_status: 24 + op_level, + }); + } + + /// Gets the client's OP level. + pub fn op_level(&self) -> u8 { + self.op_level + } + + /// Sets the last death location. The client will see + /// `minecraft:recovery_compass` items point at the provided position. + /// If the client's current dimension differs from the provided + /// dimension or the location is `None` then the compass will spin + /// randomly. + /// + /// Changes to the last death location take effect when the client + /// (re)spawns. + pub fn set_death_location(&mut self, location: Option<(DimensionId, BlockPos)>) { + self.death_location = location; + } + + pub fn trigger_status(&mut self, status: EntityStatus) { + self.write_packet(&EntityEvent { + entity_id: 0, + entity_status: status as u8, + }); + } + + /// The item that the client thinks it's holding under the mouse + /// cursor. Only relevant when the client has an open inventory. + pub fn cursor_item(&self) -> Option<&ItemStack> { + self.cursor_item.as_ref() + } + + pub fn replace_cursor_item(&mut self, item: impl Into>) -> Option { + let new = item.into(); + if self.cursor_item != new { + self.cursor_item_modified = true; + } + + std::mem::replace(&mut self.cursor_item, new) + } + + pub fn player(&self) -> &Player { + &self.player_data + } + + pub fn player_mut(&mut self) -> &mut Player { + &mut self.player_data + } + + /// Sends a system message to the player which is visible in the chat. The + /// message is only visible to this client. + pub fn send_message(&mut self, msg: impl Into) { + self.write_packet(&SystemChatMessage { + chat: msg.into().into(), + overlay: false, + }); + } + + pub fn send_plugin_message(&mut self, channel: Ident<&str>, data: &[u8]) { + self.write_packet(&PluginMessageS2c { + channel, + data: RawBytes(data), + }); + } + + /// Get the slot id in the player's inventory that the client says it's + /// holding. + pub fn held_item_slot(&self) -> u16 { + self.held_item_slot + } + + /// Kick the client with the given reason. + pub fn kick(&mut self, reason: impl Into) { + self.write_packet(&DisconnectPlay { + reason: reason.into().into(), + }); + self.is_disconnected = true; } /// Requests that the client download and enable a resource pack. @@ -817,701 +489,587 @@ impl Client { forced: bool, prompt_message: Option, ) { - self.queue_packet(&ResourcePackS2c { + self.write_packet(&ResourcePackS2c { url, hash, forced, - prompt_message, + prompt_message: prompt_message.map(|t| t.into()), }); } - /// Sets the world_age and the current in-game time. + /// Sets the title this client sees. /// - /// To stop time from passing, the `time_of_day` parameter must be - /// negative. The client stops the time at the absolute value. - pub fn set_time(&mut self, world_age: i64, time_of_day: i64) { - self.queue_packet(&UpdateTime { - world_age, - time_of_day, - }); - } - - /// Disconnects this client from the server with the provided reason. This - /// has no effect if the client is already disconnected. - /// - /// All future calls to [`Self::is_disconnected`] will return `true`. - pub fn disconnect(&mut self, reason: impl Into) { - self.queue_packet(&DisconnectPlay { - reason: reason.into(), - }); - self.disconnect_abrupt(); - } - - /// Like [`Self::disconnect`], but no reason for the disconnect is - /// sent to the client. - pub fn disconnect_abrupt(&mut self) { - self.send = None; - } - - /// Returns an immutable reference to the client's own [`Player`] data. - pub fn player(&self) -> &Player { - &self.player_data - } - - /// Returns a mutable reference to the client's own [`Player`] data. - /// - /// Changes made to this data is only visible to this client. - pub fn player_mut(&mut self) -> &mut Player { - &mut self.player_data - } - - pub fn slot(&self, idx: u16) -> Option<&ItemStack> { - self.slots - .get(idx as usize) - .expect("slot index out of range") - .as_ref() - } - - pub fn replace_slot( + /// A title is a large piece of text displayed in the center of the screen + /// which may also include a subtitle underneath it. The title can be + /// configured to fade in and out using the [`SetTitleAnimationTimes`] + /// struct. + pub fn set_title( &mut self, - idx: u16, - item: impl Into>, - ) -> Option { - assert!((idx as usize) < self.slots.len(), "slot index out of range"); - - let new = item.into(); - let old = &mut self.slots[idx as usize]; - - if new != *old { - self.modified_slots |= 1 << idx; - } - - mem::replace(old, new) - } - - pub fn cursor_item(&self) -> Option<&ItemStack> { - self.cursor_item.as_ref() - } - - pub fn replace_cursor_item(&mut self, item: impl Into>) -> Option { - let new = item.into(); - if self.cursor_item != new { - todo!("set cursor item bit"); - } - - mem::replace(&mut self.cursor_item, new) - } - - pub fn open_inventory(&self) -> Option { - self.open_inventory - } - - /// Marks the client's currently open inventory as the given inventory. - pub fn set_open_inventory(&mut self, id: InventoryId) { - if self.open_inventory != Some(id) { - self.bits.set_open_inventory_modified(true); - self.open_inventory = Some(id); - } - } - - /// Marks the client's currently open inventory as closed. - pub fn set_close_inventory(&mut self) { - if self.open_inventory.is_some() { - self.bits.set_open_inventory_modified(true); - self.open_inventory = None; - } - } - - pub fn next_event(&mut self) -> Option { - match next_event_fallible(self) { - Ok(event) => event, - Err(e) => { - warn!( - username = %self.username, - uuid = %self.uuid, - ip = %self.ip, - "failed to get next event: {e:#}" - ); - self.send = None; - None - } - } - } - - pub(crate) fn prepare_c2s_packets(&mut self) { - if !self.recv.try_recv() { - self.disconnect_abrupt(); - } - } - - pub(crate) fn update( - &mut self, - current_tick: Ticks, - shared: &SharedServer, - entities: &Entities, - worlds: &Worlds, - player_lists: &PlayerLists, - inventories: &Inventories, + title: impl Into, + subtitle: impl Into, + animation: impl Into>, ) { - if let Some(mut send) = self.send.take() { - match self.update_fallible( - &mut send, - current_tick, - shared, - entities, - worlds, - player_lists, - inventories, + let title = title.into().into(); + let subtitle = subtitle.into(); + + self.write_packet(&SetTitleText { title_text: title }); + + if !subtitle.is_empty() { + self.write_packet(&SetSubtitleText { + subtitle_text: subtitle.into(), + }); + } + + if let Some(anim) = animation.into() { + self.write_packet(&anim); + } + } + + /// Sets the action bar for this client. + /// + /// The action bar is a small piece of text displayed at the bottom of the + /// screen, above the hotbar. + pub fn set_action_bar(&mut self, text: impl Into) { + self.write_packet(&SetActionBarText { + action_bar_text: text.into().into(), + }); + } + + /// Puts a particle effect at the given position, only for this client. + /// + /// If you want to show a particle effect to all players, use + /// [`Instance::play_particle`] + /// + /// [`Instance::play_particle`]: crate::instance::Instance::play_particle + pub fn play_particle( + &mut self, + particle: &Particle, + long_distance: bool, + position: impl Into, + offset: impl Into, + max_speed: f32, + count: i32, + ) { + self.write_packet(&ParticleS2c { + particle: particle.clone(), + long_distance, + position: position.into().into(), + offset: offset.into().into(), + max_speed, + count, + }) + } +} + +impl WritePacket for Client { + fn write_packet

(&mut self, packet: &P) + where + P: EncodePacket + ?Sized, + { + self.enc.write_packet(packet) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.enc.write_packet_bytes(bytes) + } +} + +/// A system for adding [`Despawned`] components to disconnected clients. +pub fn despawn_disconnected_clients(mut commands: Commands, clients: Query<(Entity, &Client)>) { + for (entity, client) in &clients { + if client.is_disconnected() { + commands.entity(entity).insert(Despawned); + } + } +} + +pub(crate) fn update_clients( + server: Res, + mut clients: Query<(Entity, &mut Client, Option<&McEntity>)>, + instances: Query<&Instance>, + entities: Query<&McEntity>, +) { + // TODO: what batch size to use? + clients.par_for_each_mut(16, |(entity_id, mut client, self_entity)| { + if !client.is_disconnected() { + if let Err(e) = update_one_client( + &mut client, + self_entity, + entity_id, + &instances, + &entities, + &server, ) { - Ok(()) => self.send = Some(send), - Err(e) => { - let _ = send.append_packet(&DisconnectPlay { reason: "".into() }); - warn!( - username = %self.username, - uuid = %self.uuid, - ip = %self.ip, - "error updating client: {e:#}" - ); - } + client.write_packet(&DisconnectPlay { + reason: Text::from("").into(), + }); + client.is_disconnected = true; + warn!( + username = %client.username, + uuid = %client.uuid, + ip = %client.ip, + "error updating client: {e:#}" + ); } } - self.bits.set_created_this_tick(false); - } + client.is_new = false; + }); +} - /// Called by [`Self::update`] with the possibility of exiting early with an - /// error. If an error does occur, the client is abruptly disconnected and - /// the error is logged. - #[allow(clippy::too_many_arguments)] - fn update_fallible( - &mut self, - send: &mut PlayPacketSender, - current_tick: Ticks, - shared: &SharedServer, - entities: &Entities, - worlds: &Worlds, - player_lists: &PlayerLists, - inventories: &Inventories, - ) -> anyhow::Result<()> { - debug_assert!(self.entities_to_unload.is_empty()); +#[inline] +fn update_one_client( + client: &mut Client, + _self_entity: Option<&McEntity>, + _self_id: Entity, + instances: &Query<&Instance>, + entities: &Query<&McEntity>, + server: &Server, +) -> anyhow::Result<()> { + let Ok(instance) = instances.get(client.instance) else { + bail!("client is in a nonexistent instance"); + }; - let Some(world) = worlds.get(self.world) else { - bail!("client is in an invalid world") - }; + // Send the login (play) packet and other initial packets. We defer this until + // now so that the user can set the client's initial location, game + // mode, etc. + if client.is_new { + client.needs_respawn = false; - ensure!(!world.deleted(), "client is in a deleted world"); + let dimension_names: Vec<_> = server + .dimensions() + .map(|(id, _)| id.dimension_name()) + .collect(); - // Send the login (play) packet and other initial packets. We defer this until - // now so that the user can set the client's initial location, game - // mode, etc. - if self.created_this_tick() { - self.bits.set_respawn(false); + // The login packet is prepended so that it is sent before all the other + // packets. Some packets don't work correctly when sent before the login packet, + // which is why we're doing this. + client.enc.prepend_packet(&LoginPlayOwned { + entity_id: 0, // ID 0 is reserved for clients. + is_hardcore: client.is_hardcore, + game_mode: client.game_mode, + previous_game_mode: -1, + dimension_names, + registry_codec: server.registry_codec().clone(), + dimension_type_name: instance.dimension().dimension_type_name(), + dimension_name: instance.dimension().dimension_name(), + hashed_seed: 42, + max_players: VarInt(0), // Unused + view_distance: VarInt(client.view_distance() as i32), + simulation_distance: VarInt(16), + reduced_debug_info: false, + enable_respawn_screen: client.has_respawn_screen, + is_debug: false, + is_flat: client.is_flat, + last_death_location: client + .death_location + .map(|(id, pos)| (id.dimension_name(), pos)), + })?; - let dimension_names: Vec<_> = shared - .dimensions() - .map(|(id, _)| id.dimension_name()) - .collect(); + /* + // TODO: enable all the features? + send.append_packet(&FeatureFlags { + features: vec![Ident::new("vanilla").unwrap()], + })?; + */ + } else { + if client.view_distance != client.old_view_distance { + // Change the render distance fog. + client.enc.append_packet(&SetRenderDistance { + view_distance: VarInt(client.view_distance.into()), + })?; + } - // The login packet is prepended so that it is sent before all the other - // packets. Some packets don't work correctly when sent before the login packet, - // which is why we're doing this. - send.prepend_packet(&LoginPlayOwned { - entity_id: 0, // ID 0 is reserved for clients. - is_hardcore: self.bits.hardcore(), - game_mode: self.game_mode, + if client.needs_respawn { + client.needs_respawn = false; + + client.enc.append_packet(&RespawnOwned { + dimension_type_name: instance.dimension().dimension_type_name(), + dimension_name: instance.dimension().dimension_name(), + hashed_seed: 0, + game_mode: client.game_mode, previous_game_mode: -1, - dimension_names, - registry_codec: shared.registry_codec().clone(), - dimension_type_name: world.dimension().dimension_type_name(), - dimension_name: world.dimension().dimension_name(), - hashed_seed: 10, - max_players: VarInt(0), // Unused - view_distance: VarInt(self.view_distance() as i32), - simulation_distance: VarInt(16), - reduced_debug_info: false, - enable_respawn_screen: self.bits.respawn_screen(), is_debug: false, - is_flat: self.bits.flat(), - last_death_location: self + is_flat: client.is_flat, + copy_metadata: true, + last_death_location: client .death_location .map(|(id, pos)| (id.dimension_name(), pos)), })?; + } + } - /* - // TODO: enable all the features? - send.append_packet(&FeatureFlags { - features: vec![Ident::new("vanilla").unwrap()], - })?; - */ - - if let Some(id) = &self.player_list { - player_lists[id].write_init_packets(&mut *send)?; - } + // Check if it's time to send another keepalive. + if server.current_tick() % (server.tps() * 10) == 0 { + if client.got_keepalive { + let id = rand::random(); + client.enc.write_packet(&KeepAliveS2c { id }); + client.last_keepalive_id = id; + client.got_keepalive = false; } else { - if self.view_distance != self.old_view_distance { - // Change the render distance fog. - send.append_packet(&SetRenderDistance { - view_distance: VarInt(self.view_distance.into()), - })?; - } + bail!("timed out (no keepalive response)"); + } + } - if self.bits.respawn() { - self.bits.set_respawn(false); + // Send instance-wide packet data. + client.enc.append_bytes(&instance.packet_buf); - send.append_packet(&RespawnOwned { - dimension_type_name: world.dimension().dimension_type_name(), - dimension_name: world.dimension().dimension_name(), - hashed_seed: 0, - game_mode: self.game_mode(), - previous_game_mode: -1, - is_debug: false, - is_flat: self.bits.flat(), - copy_metadata: true, - last_death_location: self - .death_location - .map(|(id, pos)| (id.dimension_name(), pos)), - })?; - } + let old_view = client.old_view(); + let view = client.view(); - // If the player list was changed... - if self.old_player_list != self.player_list { - // Delete existing entries from old player list. - if let Some(id) = &self.old_player_list { - player_lists[id].write_clear_packets(&mut *send)?; + // Make sure the center chunk is set before loading chunks! + if old_view.pos != view.pos { + // TODO: does the client initialize the center chunk to (0, 0)? + client.enc.write_packet(&SetCenterChunk { + chunk_x: VarInt(view.pos.x), + chunk_z: VarInt(view.pos.z), + }); + } + + // Iterate over all visible chunks from the previous tick. + if let Ok(old_instance) = instances.get(client.old_instance) { + old_view.for_each(|pos| { + if let Some(cell) = old_instance.partition.get(&pos) { + if cell.chunk_removed && cell.chunk.is_none() { + // Chunk was previously loaded and is now deleted. + client.enc.write_packet(&UnloadChunk { + chunk_x: pos.x, + chunk_z: pos.z, + }); } - // Get initial packets for new player list. - if let Some(id) = &self.player_list { - player_lists[id].write_init_packets(&mut *send)?; + if let Some(chunk) = &cell.chunk { + chunk.mark_viewed(); } - self.old_player_list = self.player_list.clone(); - } else if let Some(id) = &self.player_list { - // Otherwise, update current player list. - player_lists[id].write_update_packets(&mut *send)?; + // Send entity spawn packets for entities entering the client's view. + for &(id, src_pos) in &cell.incoming { + if src_pos.map_or(true, |p| !old_view.contains(p)) { + // The incoming entity originated from outside the view distance, so it + // must be spawned. + if let Ok(entity) = entities.get(id) { + // Spawn the entity at the old position so that later relative entity + // movement packets will not set the entity to the wrong position. + entity.write_init_packets( + &mut client.enc, + entity.old_position(), + &mut client.scratch, + ); + } + } + } + + // Send entity despawn packets for entities exiting the client's view. + for &(id, dest_pos) in &cell.outgoing { + if dest_pos.map_or(true, |p| !old_view.contains(p)) { + // The outgoing entity moved outside the view distance, so it must be + // despawned. + if let Ok(entity) = entities.get(id) { + client + .entities_to_despawn + .push(VarInt(entity.protocol_id())); + } + } + } + + // Send all data in the chunk's packet buffer to this client. This will update + // entities in the cell, spawn or update the chunk in the cell, or send any + // other packet data that was added here by users. + client.enc.append_bytes(&cell.packet_buf); } + }); + } + + // Was the client's instance changed? + if client.old_instance != client.instance { + if let Ok(old_instance) = instances.get(client.old_instance) { + // TODO: only send unload packets when old dimension == new dimension, since the + // client will do the unloading for us in that case? + + // Unload all chunks and entities in the old view. + old_view.for_each(|pos| { + if let Some(cell) = old_instance.partition.get(&pos) { + // Unload the chunk at this cell if it was loaded. + if cell.chunk.is_some() { + client.enc.write_packet(&UnloadChunk { + chunk_x: pos.x, + chunk_z: pos.z, + }); + } + + // Unload all the entities in the cell. + for &id in &cell.entities { + if let Ok(entity) = entities.get(id) { + client + .entities_to_despawn + .push(VarInt(entity.protocol_id())); + } + } + } + }); } - // Check if it's time to send another keepalive. - if current_tick % (shared.tick_rate() * 10) == 0 { - if self.bits.got_keepalive() { - let id = rand::random(); - send.append_packet(&KeepAliveS2c { id })?; - self.last_keepalive_id = id; - self.bits.set_got_keepalive(false); + // Load all chunks and entities in new view. + view.for_each(|pos| { + if let Some(cell) = instance.partition.get(&pos) { + // Load the chunk at this cell if there is one. + if let Some(chunk) = &cell.chunk { + chunk.write_init_packets( + &instance.info, + pos, + &mut client.enc, + &mut client.scratch, + ); + + chunk.mark_viewed(); + } + + // Load all the entities in this cell. + for &id in &cell.entities { + if let Ok(entity) = entities.get(id) { + entity.write_init_packets( + &mut client.enc, + entity.position(), + &mut client.scratch, + ); + } + } + } + }); + } else if old_view != view { + // Client changed their view without changing the instance. + + // Unload chunks and entities in the old view and load chunks and entities in + // the new view. We don't need to do any work where the old and new view + // overlap. + old_view.diff_for_each(view, |pos| { + if let Some(cell) = instance.partition.get(&pos) { + // Unload the chunk at this cell if it was loaded. + if cell.chunk.is_some() { + client.enc.write_packet(&UnloadChunk { + chunk_x: pos.x, + chunk_z: pos.z, + }); + } + + // Unload all the entities in the cell. + for &id in &cell.entities { + if let Ok(entity) = entities.get(id) { + client + .entities_to_despawn + .push(VarInt(entity.protocol_id())); + } + } + } + }); + + view.diff_for_each(old_view, |pos| { + if let Some(cell) = instance.partition.get(&pos) { + // Load the chunk at this cell if there is one. + if let Some(chunk) = &cell.chunk { + chunk.write_init_packets( + &instance.info, + pos, + &mut client.enc, + &mut client.scratch, + ); + + chunk.mark_viewed(); + } + + // Load all the entities in this cell. + for &id in &cell.entities { + if let Ok(entity) = entities.get(id) { + entity.write_init_packets( + &mut client.enc, + entity.position(), + &mut client.scratch, + ); + } + } + } + }); + } + + // Despawn all the entities that are queued to be despawned. + if !client.entities_to_despawn.is_empty() { + client.enc.append_packet(&RemoveEntitiesEncode { + entity_ids: &client.entities_to_despawn, + })?; + + client.entities_to_despawn.clear(); + } + + // Teleport the client. Do this after chunk packets are sent so the client does + // not accidentally pass through blocks. + if client.position_modified || client.yaw_modified || client.pitch_modified { + let flags = SyncPlayerPosLookFlags::new() + .with_x(!client.position_modified) + .with_y(!client.position_modified) + .with_z(!client.position_modified) + .with_y_rot(!client.yaw_modified) + .with_x_rot(!client.pitch_modified); + + client.enc.write_packet(&SynchronizePlayerPosition { + position: if client.position_modified { + client.position.to_array() } else { - bail!("timed out (no keepalive response)"); + [0.0; 3] + }, + yaw: if client.yaw_modified { client.yaw } else { 0.0 }, + pitch: if client.pitch_modified { + client.pitch + } else { + 0.0 + }, + flags, + teleport_id: VarInt(client.teleport_id_counter as i32), + dismount_vehicle: false, + }); + + client.pending_teleports = client.pending_teleports.wrapping_add(1); + client.teleport_id_counter = client.teleport_id_counter.wrapping_add(1); + + client.position_modified = false; + client.yaw_modified = false; + client.pitch_modified = false; + } + + // This closes the "downloading terrain" screen. + // Send this after the initial chunks are loaded. + if client.is_new { + client.enc.write_packet(&SetDefaultSpawnPosition { + position: BlockPos::at(client.position), + angle: client.yaw, + }); + } + + // Update the client's own player metadata. + client.scratch.clear(); + client.player_data.updated_tracked_data(&mut client.scratch); + if !client.scratch.is_empty() { + client.player_data.clear_modifications(); + + client.scratch.push(0xff); + + client.enc.write_packet(&SetEntityMetadata { + entity_id: VarInt(0), + metadata: RawBytes(&client.scratch), + }); + } + + // Acknowledge broken/placed blocks. + if client.block_change_sequence != 0 { + client.enc.write_packet(&AcknowledgeBlockChange { + sequence: VarInt(client.block_change_sequence), + }); + + client.block_change_sequence = 0; + } + + client.old_instance = client.instance; + client.old_position = client.position; + client.old_view_distance = client.view_distance; + + client + .conn + .try_send(client.enc.take()) + .context("failed to flush packet queue")?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use bevy_app::App; + use valence_protocol::packets::s2c::play::ChunkDataAndUpdateLight; + use valence_protocol::packets::S2cPlayPacket; + + use super::*; + use crate::instance::Chunk; + use crate::unit_test::util::scenario_single_client; + + #[test] + fn client_chunk_view_change() { + let mut app = App::new(); + + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + + let mut instance = app + .world + .query::<&mut Instance>() + .single_mut(&mut app.world); + + for z in -15..15 { + for x in -15..15 { + instance.insert_chunk([x, z], Chunk::default()); } } - let self_entity_pos; - let self_entity_world; - let self_entity_range; + let mut client = app.world.get_mut::(client_ent).unwrap(); - // Get the entity with the same UUID as the client (if it exists). - if let Some(entity) = entities.get(self.self_entity) { - self_entity_pos = ChunkPos::at(entity.position().x, entity.position().z); - self_entity_world = entity.world(); - self_entity_range = entity.self_update_range.clone(); - } else if let Some(id) = entities.get_with_uuid(self.uuid) { - self.self_entity = id; - let entity = &entities[id]; - self_entity_pos = ChunkPos::at(entity.position().x, entity.position().z); - self_entity_world = entity.world(); - self_entity_range = entity.self_update_range.clone(); - } else { - // There is no entity with the same UUID as the client. - self_entity_pos = ChunkPos::new(0, 0); - self_entity_world = WorldId::NULL; - self_entity_range = 0..0; - } + client.set_position([8.0, 0.0, 8.0]); + client.set_view_distance(6); - let old_chunk_pos = ChunkPos::at(self.old_position.x, self.old_position.z); - let chunk_pos = ChunkPos::at(self.position.x, self.position.z); + // Tick + app.update(); + let mut client = app.world.get_mut::(client_ent).unwrap(); - // Make sure the center chunk is set before loading chunks! - if old_chunk_pos != chunk_pos { - // TODO: does the client initialize the center chunk to (0, 0)? - send.append_packet(&SetCenterChunk { - chunk_x: VarInt(chunk_pos.x), - chunk_z: VarInt(chunk_pos.z), - })?; - } + let mut loaded_chunks = BTreeSet::new(); - // Iterate over all visible chunks from the previous tick. - if let Some(old_world) = worlds.get(self.old_world) { - old_chunk_pos.try_for_each_in_view(self.old_view_distance, |pos| { - if let Some((chunk, cell)) = old_world.chunks.chunk_and_cell(pos) { - if let Some(chunk) = chunk { - // Decide if the chunk should be loaded, unloaded, or updated. - match (chunk.created_this_tick(), chunk.deleted()) { - (false, false) => { - // Update the chunk. - chunk.write_block_change_packets(&mut *send)?; - } - (true, false) => { - // Chunk needs initialization. Send packet to load it. - chunk.write_chunk_data_packet( - &mut *send, - &mut self.scratch, - pos, - &old_world.chunks, - )?; - - // Don't assert that the chunk is already loaded in this case. - // Chunks are allowed to be overwritten and their "created this - // tick" flag will become true again. - #[cfg(debug_assertions)] - self.loaded_chunks.insert(pos); - } - (false, true) => { - // Chunk was previously loaded and is now deleted. - send.append_packet(&UnloadChunk { - chunk_x: pos.x, - chunk_z: pos.z, - })?; - - #[cfg(debug_assertions)] - assert!(self.loaded_chunks.remove(&pos)); - } - (true, true) => { - // Chunk was created and deleted this tick, so - // we don't need to do anything. - } - } - } - - // Send entity spawn packets for entities entering the client's view. - for &(id, src_pos) in cell.incoming() { - if src_pos.map_or(true, |p| { - !old_chunk_pos.is_in_view(p, self.old_view_distance) - }) { - // The incoming entity originated from outside the view distance, so it - // must be spawned. - let entity = &entities[id]; - debug_assert!(!entity.deleted()); - - if entity.uuid() != self.uuid { - // Spawn the entity at the old position so that relative entity - // movement packets will not set the entity to the wrong position. - entity.send_init_packets( - send, - entity.old_position(), - id, - &mut self.scratch, - )?; - } - } - } - - // Send entity despawn packets for entities exiting the client's view. - for &(id, dest_pos) in cell.outgoing() { - if id != self.self_entity - && dest_pos.map_or(true, |p| { - !old_chunk_pos.is_in_view(p, self.old_view_distance) - }) - { - // The outgoing entity moved outside the view distance, so it must be - // despawned. - self.entities_to_unload.push(VarInt(id.to_raw())); - } - } - - // Update all the entities in the chunk. - if pos == self_entity_pos && self.old_world == self_entity_world { - // Don't update the entity with the same UUID as the client. - let bytes = cell.cached_update_packets(); - send.append_bytes(&bytes[..self_entity_range.start]); - send.append_bytes(&bytes[self_entity_range.end..]); - } else { - send.append_bytes(cell.cached_update_packets()); - } - } - - Ok(()) - })?; - - if !self.entities_to_unload.is_empty() { - send.append_packet(&RemoveEntitiesEncode { - entity_ids: &self.entities_to_unload, - })?; - self.entities_to_unload.clear(); - } - } - - if self.old_world != self.world { - // Client changed the world they're in. - - // Unload all chunks and entities in old view. - if let Some(old_world) = worlds.get(self.old_world) { - // TODO: only send unload packets when old dimension == new dimension, since the - // client will do the unloading for us in that case? - - old_chunk_pos.try_for_each_in_view(self.old_view_distance, |pos| { - if let Some((chunk, cell)) = old_world.chunks.chunk_and_cell(pos) { - if let Some(chunk) = chunk { - // Deleted chunks were already unloaded above. - if !chunk.deleted() { - send.append_packet(&UnloadChunk { - chunk_x: pos.x, - chunk_z: pos.z, - })?; - - #[cfg(debug_assertions)] - assert!(self.loaded_chunks.remove(&pos)); - } - } - - self.entities_to_unload.extend( - cell.entities() - .filter(|&id| id != self.self_entity) - .map(|id| VarInt(id.to_raw())), - ); - } - - Ok(()) - })?; - - if !self.entities_to_unload.is_empty() { - send.append_packet(&RemoveEntitiesEncode { - entity_ids: &self.entities_to_unload, - })?; - self.entities_to_unload.clear(); - } - } - - // Load all chunks and entities in new view. - chunk_pos.try_for_each_in_view(self.view_distance, |pos| { - if let Some((chunk, cell)) = world.chunks.chunk_and_cell(pos) { - if let Some(chunk) = chunk { - if !chunk.deleted() { - chunk.write_chunk_data_packet( - &mut *send, - &mut self.scratch, - pos, - &world.chunks, - )?; - - #[cfg(debug_assertions)] - assert!(self.loaded_chunks.insert(pos)); - } - } - - for id in cell.entities() { - let entity = &entities[id]; - debug_assert!(!entity.deleted()); - - if entity.uuid() != self.uuid { - entity.send_init_packets( - send, - entity.position(), - id, - &mut self.scratch, - )?; - } - } - } - - Ok(()) - })?; - } else if old_chunk_pos != chunk_pos || self.old_view_distance != self.view_distance { - // Client changed their view without changing the world. - // We need to unload chunks and entities in the old view and load - // chunks and entities in the new view. We don't need to do any - // work where the old and new view overlap. - - old_chunk_pos.try_for_each_in_view(self.old_view_distance, |pos| { - if !pos.is_in_view(chunk_pos, self.view_distance) { - if let Some((chunk, cell)) = world.chunks.chunk_and_cell(pos) { - if let Some(chunk) = chunk { - // Deleted chunks were already unloaded above. - if !chunk.deleted() { - send.append_packet(&UnloadChunk { - chunk_x: pos.x, - chunk_z: pos.z, - })?; - - #[cfg(debug_assertions)] - assert!(self.loaded_chunks.remove(&pos)); - } - } - - self.entities_to_unload.extend( - cell.entities() - .filter(|&id| id != self.self_entity) - .map(|id| VarInt(id.to_raw())), - ); - } - } - - Ok(()) - })?; - - if !self.entities_to_unload.is_empty() { - send.append_packet(&RemoveEntitiesEncode { - entity_ids: &self.entities_to_unload, - })?; - self.entities_to_unload.clear(); - } - - chunk_pos.try_for_each_in_view(self.view_distance, |pos| { - if !pos.is_in_view(old_chunk_pos, self.old_view_distance) { - if let Some((chunk, cell)) = world.chunks.chunk_and_cell(pos) { - if let Some(chunk) = chunk { - if !chunk.deleted() { - chunk.write_chunk_data_packet( - &mut *send, - &mut self.scratch, - pos, - &world.chunks, - )?; - - #[cfg(debug_assertions)] - assert!(self.loaded_chunks.insert(pos)); - } - } - - for id in cell.entities() { - let entity = &entities[id]; - debug_assert!(!entity.deleted()); - - if entity.uuid() != self.uuid { - entity.send_init_packets( - send, - entity.position(), - id, - &mut self.scratch, - )?; - } - } - } - } - - Ok(()) - })?; - } - - if self.bits.created_this_tick() { - // This closes the "downloading terrain" screen. - // Send this after the initial chunks are loaded. - send.append_packet(&SetDefaultSpawnPosition { - position: BlockPos::at(self.position.into_array()), - angle: self.yaw, - })?; - } - - // Update the client's own player metadata. - self.scratch.clear(); - self.player_data.updated_tracked_data(&mut self.scratch); - if !self.scratch.is_empty() { - self.scratch.push(0xff); - - send.append_packet(&SetEntityMetadata { - entity_id: VarInt(0), - metadata: RawBytes(&self.scratch), - })?; - } - - // Acknowledge broken/placed blocks. - if self.block_change_sequence != 0 { - send.append_packet(&AcknowledgeBlockChange { - sequence: VarInt(self.block_change_sequence), - })?; - - self.block_change_sequence = 0; - } - - // TODO: inventory stuff below is incomplete. - - // Update the client's own inventory. - if self.modified_slots != 0 { - if self.created_this_tick() - || self.modified_slots == u64::MAX && self.bits.cursor_item_modified() + for pkt in client_helper.collect_sent().unwrap() { + if let S2cPlayPacket::ChunkDataAndUpdateLight(ChunkDataAndUpdateLight { + chunk_x, + chunk_z, + .. + }) = pkt { - // Update the whole inventory. - send.append_packet(&SetContainerContentEncode { - window_id: 0, - state_id: VarInt(self.inv_state_id.0), - slots: self.slots.as_slice(), - carried_item: &self.cursor_item, - })?; - - self.inv_state_id += 1; - self.bits.set_cursor_item_modified(false); - } else { - // Update only the slots that were modified. - for (i, slot) in self.slots.iter().enumerate() { - if (self.modified_slots >> i) & 1 == 1 { - send.append_packet(&SetContainerSlotEncode { - window_id: 0, - state_id: VarInt(self.inv_state_id.0), - slot_idx: i as i16, - slot_data: slot.as_ref(), - })?; - - self.inv_state_id += 1; - } - } - } - - self.modified_slots = 0; - } - - if self.bits.cursor_item_modified() { - self.bits.set_cursor_item_modified(false); - - send.append_packet(&SetContainerSlotEncode { - window_id: -1, - state_id: VarInt(self.inv_state_id.0), - slot_idx: -1, - slot_data: self.cursor_item.as_ref(), - })?; - - self.inv_state_id += 1; - } - - // Update the window the client has opened. - if let Some(inv_id) = self.open_inventory { - if let Some(inv) = inventories.get(inv_id) { - if self.bits.open_inventory_modified() { - // Open a new window. - self.bits.set_open_inventory_modified(false); - - self.window_id = self.window_id % 100 + 1; - self.inv_state_id += 1; - - send.append_packet(&OpenScreen { - window_id: VarInt(self.window_id.into()), - window_type: VarInt(inv.kind() as i32), - window_title: inv.title().clone(), - })?; - - send.append_packet(&SetContainerContentEncode { - window_id: self.window_id, - state_id: VarInt(self.inv_state_id.0), - slots: inv.slot_slice(), - carried_item: &self.cursor_item, - })?; - } else { - // Update an already open window. - inv.send_update(send, self.window_id, &mut self.inv_state_id)?; - } - } else { - // the inventory no longer exists, so close the window - send.append_packet(&CloseContainerS2c { - window_id: self.window_id, - })?; - self.open_inventory = None; // avoids setting the modified flag + assert!( + loaded_chunks.insert(ChunkPos::new(chunk_x, chunk_z)), + "({chunk_x}, {chunk_z})" + ); } } - self.old_world = self.world; - self.old_position = self.position; - self.old_view_distance = self.view_distance; - self.player_data.clear_modifications(); + for pos in client.view().iter() { + assert!(loaded_chunks.contains(&pos), "{pos:?}"); + } - send.flush().context("failed to flush packet queue")?; + assert!(!loaded_chunks.is_empty()); - Ok(()) + // Move the client to the adjacent chunk. + client.set_position([24.0, 0.0, 24.0]); + + // Tick + app.update(); + let client = app.world.get_mut::(client_ent).unwrap(); + + for pkt in client_helper.collect_sent().unwrap() { + match pkt { + S2cPlayPacket::ChunkDataAndUpdateLight(ChunkDataAndUpdateLight { + chunk_x, + chunk_z, + .. + }) => { + assert!( + loaded_chunks.insert(ChunkPos::new(chunk_x, chunk_z)), + "({chunk_x}, {chunk_z})" + ); + } + S2cPlayPacket::UnloadChunk(UnloadChunk { chunk_x, chunk_z }) => { + assert!( + loaded_chunks.remove(&ChunkPos::new(chunk_x, chunk_z)), + "({chunk_x}, {chunk_z})" + ); + } + _ => {} + } + } + + for pos in client.view().iter() { + assert!(loaded_chunks.contains(&pos), "{pos:?}"); + } } } diff --git a/crates/valence/src/client/event.rs b/crates/valence/src/client/event.rs index 38bc7e9..204397a 100644 --- a/crates/valence/src/client/event.rs +++ b/crates/valence/src/client/event.rs @@ -1,6 +1,12 @@ use std::cmp; use anyhow::bail; +use bevy_ecs::prelude::*; +use bevy_ecs::schedule::ShouldRun; +use bevy_ecs::system::SystemParam; +use glam::{DVec3, Vec3}; +use paste::paste; +use tracing::warn; use uuid::Uuid; use valence_protocol::entity_meta::Pose; use valence_protocol::packets::c2s::play::{ @@ -12,331 +18,819 @@ use valence_protocol::types::{ DisplayedSkinParts, EntityInteraction, Hand, MainHand, RecipeBookId, StructureBlockAction, StructureBlockFlags, StructureBlockMirror, StructureBlockMode, StructureBlockRotation, }; -use valence_protocol::{BlockFace, BlockPos, Ident, ItemStack, VarLong}; +use valence_protocol::{BlockFace, BlockPos, Ident, ItemStack}; use crate::client::Client; -use crate::config::Config; -use crate::entity::{Entity, EntityEvent, TrackedData}; +use crate::entity::{EntityAnimation, EntityKind, McEntity, TrackedData}; -/// A discrete action performed by a client. -/// -/// Client events are a more convenient representation of the data contained in -/// a [`C2sPlayPacket`]. -/// -/// [`C2sPlayPacket`]: crate::protocol::packets::C2sPlayPacket #[derive(Clone, Debug)] -pub enum ClientEvent { - QueryBlockEntity { - position: BlockPos, - transaction_id: i32, - }, - ChangeDifficulty(Difficulty), - MessageAcknowledgment { - message_count: i32, - }, - ChatCommand { - command: Box, - timestamp: u64, - }, - ChatMessage { - message: Box, - timestamp: u64, - }, - ChatPreview, - PerformRespawn, - RequestStats, - UpdateSettings { - /// e.g. en_US - locale: Box, - /// The client side render distance, in chunks. - /// - /// The value is always in `2..=32`. - view_distance: u8, - chat_mode: ChatMode, - /// `true` if the client has chat colors enabled, `false` otherwise. - chat_colors: bool, - displayed_skin_parts: DisplayedSkinParts, - main_hand: MainHand, - enable_text_filtering: bool, - allow_server_listings: bool, - }, - CommandSuggestionsRequest { - transaction_id: i32, - text: Box, - }, - ClickContainerButton { - window_id: i8, - button_id: i8, - }, - ClickContainer { - window_id: u8, - state_id: i32, - slot_id: i16, - button: i8, - mode: ClickContainerMode, - slot_changes: Vec<(i16, Option)>, - carried_item: Option, - }, - CloseContainer { - window_id: i8, - }, - PluginMessage { - channel: Ident>, - data: Box<[u8]>, - }, - EditBook { - slot: i32, - entries: Vec>, - title: Option>, - }, - QueryEntity { - transaction_id: i32, - entity_id: i32, - }, - /// Left or right click interaction with an entity's hitbox. - InteractWithEntity { - /// The raw ID of the entity being interacted with. - entity_id: i32, - /// If the client was sneaking during the interaction. - sneaking: bool, - /// The kind of interaction that occurred. - interact: EntityInteraction, - }, - JigsawGenerate { - position: BlockPos, - levels: i32, - keep_jigsaws: bool, - }, - LockDifficulty(bool), - // TODO: combine movement events? - SetPlayerPosition { - position: [f64; 3], - on_ground: bool, - }, - SetPlayerPositionAndRotation { - position: [f64; 3], - yaw: f32, - pitch: f32, - on_ground: bool, - }, - SetPlayerRotation { - yaw: f32, - pitch: f32, - on_ground: bool, - }, - SetPlayerOnGround(bool), - MoveVehicle { - position: [f64; 3], - yaw: f32, - pitch: f32, - }, - StartSneaking, - StopSneaking, - LeaveBed, - StartSprinting, - StopSprinting, - StartJumpWithHorse { - /// The power of the horse jump in `0..=100`. - jump_boost: u8, - }, - /// A jump while on a horse stopped. - StopJumpWithHorse, - /// The inventory was opened while on a horse. - OpenHorseInventory, - StartFlyingWithElytra, - PaddleBoat { - left_paddle_turning: bool, - right_paddle_turning: bool, - }, - PickItem { - slot_to_use: i32, - }, - PlaceRecipe { - window_id: i8, - recipe: Ident>, - make_all: bool, - }, - StopFlying, - StartFlying, - StartDigging { - position: BlockPos, - face: BlockFace, - sequence: i32, - }, - CancelDigging { - position: BlockPos, - face: BlockFace, - sequence: i32, - }, - FinishDigging { - position: BlockPos, - face: BlockFace, - sequence: i32, - }, - DropItem, - DropItemStack, - /// Eating food, pulling back bows, using buckets, etc. - UpdateHeldItemState, - SwapItemInHand, - PlayerInput { - sideways: f32, - forward: f32, - jump: bool, - unmount: bool, - }, - Pong { - id: i32, - }, - PlayerSession { - session_id: Uuid, - expires_at: i64, - public_key_data: Box<[u8]>, - key_signature: Box<[u8]>, - }, - ChangeRecipeBookSettings { - book_id: RecipeBookId, - book_open: bool, - filter_active: bool, - }, - SetSeenRecipe { - recipe_id: Ident>, - }, - RenameItem { - name: Box, - }, - ResourcePackLoaded, - ResourcePackDeclined, - ResourcePackFailedDownload, - ResourcePackAccepted, - OpenAdvancementTab { - tab_id: Ident>, - }, - CloseAdvancementScreen, - SelectTrade { - slot: i32, - }, - SetBeaconEffect { - primary_effect: Option, - secondary_effect: Option, - }, - SetHeldItem { - slot: i16, - }, - ProgramCommandBlock { - position: BlockPos, - command: Box, - mode: CommandBlockMode, - track_output: bool, - conditional: bool, - automatic: bool, - }, - ProgramCommandBlockMinecart { - entity_id: i32, - command: Box, - track_output: bool, - }, - SetCreativeModeSlot { - slot: i16, - clicked_item: Option, - }, - ProgramJigsawBlock { - position: BlockPos, - name: Ident>, - target: Ident>, - pool: Ident>, - final_state: Box, - joint_type: Box, - }, - ProgramStructureBlock { - position: BlockPos, - action: StructureBlockAction, - mode: StructureBlockMode, - name: Box, - offset_xyz: [i8; 3], - size_xyz: [i8; 3], - mirror: StructureBlockMirror, - rotation: StructureBlockRotation, - metadata: Box, - integrity: f32, - seed: VarLong, - flags: StructureBlockFlags, - }, - UpdateSign { - position: BlockPos, - lines: [Box; 4], - }, - SwingArm(Hand), - TeleportToEntity { - target: Uuid, - }, - UseItemOnBlock { - /// The hand that was used - hand: Hand, - /// The location of the block that was interacted with - position: BlockPos, - /// The face of the block that was clicked - face: BlockFace, - /// The position inside of the block that was clicked on - cursor_pos: [f32; 3], - /// Whether or not the player's head is inside a block - head_inside_block: bool, - /// Sequence number for synchronization - sequence: i32, - }, - UseItem { - hand: Hand, - sequence: i32, - }, +pub struct QueryBlockEntity { + pub client: Entity, + pub position: BlockPos, + pub transaction_id: i32, } -pub(super) fn next_event_fallible( - client: &mut Client, -) -> anyhow::Result> { - loop { - let Some(pkt) = client.recv.try_next_packet::()? else { - return Ok(None) - }; +#[derive(Clone, Debug)] +pub struct ChangeDifficulty { + pub client: Entity, + pub difficulty: Difficulty, +} - return Ok(Some(match pkt { - C2sPlayPacket::ConfirmTeleport(p) => { - if client.pending_teleports == 0 { - bail!("unexpected teleport confirmation"); +#[derive(Clone, Debug)] +pub struct MessageAcknowledgment { + pub client: Entity, + pub message_count: i32, +} + +#[derive(Clone, Debug)] +pub struct ChatCommand { + pub client: Entity, + pub command: Box, + pub timestamp: u64, +} + +#[derive(Clone, Debug)] +pub struct ChatMessage { + pub client: Entity, + pub message: Box, + pub timestamp: u64, +} + +#[derive(Clone, Debug)] +pub struct ChatPreview { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct PerformRespawn { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct RequestStats { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct UpdateSettings { + pub client: Entity, + /// e.g. en_US + pub locale: Box, + /// The client side render distance, in chunks. + /// + /// The value is always in `2..=32`. + pub view_distance: u8, + pub chat_mode: ChatMode, + /// `true` if the client has chat colors enabled, `false` otherwise. + pub chat_colors: bool, + pub displayed_skin_parts: DisplayedSkinParts, + pub main_hand: MainHand, + pub enable_text_filtering: bool, + pub allow_server_listings: bool, +} + +#[derive(Clone, Debug)] +pub struct CommandSuggestionsRequest { + pub client: Entity, + pub transaction_id: i32, + pub text: Box, +} + +#[derive(Clone, Debug)] +pub struct ClickContainerButton { + pub client: Entity, + pub window_id: i8, + pub button_id: i8, +} + +#[derive(Clone, Debug)] +pub struct ClickContainer { + pub client: Entity, + pub window_id: u8, + pub state_id: i32, + pub slot_id: i16, + pub button: i8, + pub mode: ClickContainerMode, + pub slot_changes: Vec<(i16, Option)>, + pub carried_item: Option, +} + +#[derive(Clone, Debug)] +pub struct CloseContainer { + pub client: Entity, + pub window_id: i8, +} + +#[derive(Clone, Debug)] +pub struct PluginMessage { + pub client: Entity, + pub channel: Ident>, + pub data: Box<[u8]>, +} + +#[derive(Clone, Debug)] +pub struct EditBook { + pub slot: i32, + pub entries: Vec>, + pub title: Option>, +} + +#[derive(Clone, Debug)] +pub struct QueryEntityTag { + pub client: Entity, + pub transaction_id: i32, + pub entity_id: i32, +} + +/// Left or right click interaction with an entity's hitbox. +#[derive(Clone, Debug)] +pub struct InteractWithEntity { + pub client: Entity, + /// The raw ID of the entity being interacted with. + pub entity_id: i32, + /// If the client was sneaking during the interaction. + pub sneaking: bool, + /// The kind of interaction that occurred. + pub interact: EntityInteraction, +} + +#[derive(Clone, Debug)] +pub struct JigsawGenerate { + pub client: Entity, + pub position: BlockPos, + pub levels: i32, + pub keep_jigsaws: bool, +} + +#[derive(Clone, Debug)] +pub struct LockDifficulty { + pub client: Entity, + pub locked: bool, +} + +#[derive(Clone, Debug)] +pub struct SetPlayerPosition { + pub client: Entity, + pub position: DVec3, + pub on_ground: bool, +} + +#[derive(Clone, Debug)] +pub struct SetPlayerPositionAndRotation { + pub client: Entity, + pub position: DVec3, + pub yaw: f32, + pub pitch: f32, + pub on_ground: bool, +} + +#[derive(Clone, Debug)] +pub struct SetPlayerRotation { + pub client: Entity, + pub yaw: f32, + pub pitch: f32, + pub on_ground: bool, +} + +#[derive(Clone, Debug)] +pub struct SetPlayerOnGround { + pub client: Entity, + pub on_ground: bool, +} + +#[derive(Clone, Debug)] +pub struct MoveVehicle { + pub client: Entity, + pub position: DVec3, + pub yaw: f32, + pub pitch: f32, +} + +/// Sent whenever one of the other movement events is sent. +#[derive(Clone, Debug)] +pub struct MovePlayer { + pub client: Entity, + /// The position of the client prior to the event. + pub old_position: DVec3, + /// The position of the client after the event. + pub position: DVec3, + /// The yaw of the client prior to the event. + pub old_yaw: f32, + /// The yaw of the client after the event. + pub yaw: f32, + /// The pitch of the client prior to the event. + pub old_pitch: f32, + /// The pitch of the client after the event. + pub pitch: f32, + /// If the client was on ground prior to the event. + pub old_on_ground: bool, + /// If the client is on ground after the event. + pub on_ground: bool, +} + +#[derive(Clone, Debug)] +pub struct StartSneaking { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct StopSneaking { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct LeaveBed { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct StartSprinting { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct StopSprinting { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct StartJumpWithHorse { + pub client: Entity, + /// The power of the horse jump in `0..=100`. + pub jump_boost: u8, +} + +#[derive(Clone, Debug)] +pub struct StopJumpWithHorse { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct OpenHorseInventory { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct StartFlyingWithElytra { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct PaddleBoat { + pub client: Entity, + pub left_paddle_turning: bool, + pub right_paddle_turning: bool, +} + +#[derive(Clone, Debug)] +pub struct PickItem { + pub client: Entity, + pub slot_to_use: i32, +} + +#[derive(Clone, Debug)] +pub struct PlaceRecipe { + pub client: Entity, + pub window_id: i8, + pub recipe: Ident>, + pub make_all: bool, +} + +#[derive(Clone, Debug)] +pub struct StopFlying { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct StartFlying { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct StartDigging { + pub client: Entity, + pub position: BlockPos, + pub face: BlockFace, + pub sequence: i32, +} + +#[derive(Clone, Debug)] +pub struct CancelDigging { + pub client: Entity, + pub position: BlockPos, + pub face: BlockFace, + pub sequence: i32, +} + +#[derive(Clone, Debug)] +pub struct FinishDigging { + pub client: Entity, + pub position: BlockPos, + pub face: BlockFace, + pub sequence: i32, +} + +#[derive(Clone, Debug)] +pub struct DropItem { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct DropItemStack { + pub client: Entity, +} + +/// Eating food, pulling back bows, using buckets, etc. +#[derive(Clone, Debug)] +pub struct UpdateHeldItemState { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct SwapItemInHand { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct PlayerInput { + pub client: Entity, + pub sideways: f32, + pub forward: f32, + pub jump: bool, + pub unmount: bool, +} + +#[derive(Clone, Debug)] +pub struct Pong { + pub client: Entity, + pub id: i32, +} + +#[derive(Clone, Debug)] +pub struct PlayerSession { + pub client: Entity, + pub session_id: Uuid, + pub expires_at: i64, + pub public_key_data: Box<[u8]>, + pub key_signature: Box<[u8]>, +} + +#[derive(Clone, Debug)] +pub struct ChangeRecipeBookSettings { + pub client: Entity, + pub book_id: RecipeBookId, + pub book_open: bool, + pub filter_active: bool, +} + +#[derive(Clone, Debug)] +pub struct SetSeenRecipe { + pub client: Entity, + pub recipe_id: Ident>, +} + +#[derive(Clone, Debug)] +pub struct RenameItem { + pub client: Entity, + pub name: Box, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum ResourcePackStatus { + /// The client has accepted the server's resource pack. + Accepted, + /// The client has declined the server's resource pack. + Declined, + /// The client has successfully loaded the server's resource pack. + Loaded, + /// The client has failed to download the server's resource pack. + FailedDownload, +} + +impl From for ResourcePackStatus { + fn from(packet: ResourcePackC2s) -> Self { + match packet { + ResourcePackC2s::Accepted { .. } => Self::Accepted, + ResourcePackC2s::Declined { .. } => Self::Declined, + ResourcePackC2s::SuccessfullyLoaded { .. } => Self::Loaded, + ResourcePackC2s::FailedDownload { .. } => Self::FailedDownload, + } + } +} + +#[derive(Clone, Debug)] +pub struct ResourcePackStatusChange { + pub client: Entity, + pub status: ResourcePackStatus, +} + +#[derive(Clone, Debug)] +pub struct OpenAdvancementTab { + pub client: Entity, + pub tab_id: Ident>, +} + +#[derive(Clone, Debug)] +pub struct CloseAdvancementScreen { + pub client: Entity, +} + +#[derive(Clone, Debug)] +pub struct SelectTrade { + pub client: Entity, + pub slot: i32, +} + +#[derive(Clone, Debug)] +pub struct SetBeaconEffect { + pub client: Entity, + pub primary_effect: Option, + pub secondary_effect: Option, +} + +#[derive(Clone, Debug)] +pub struct SetHeldItem { + pub client: Entity, + pub slot: i16, +} + +#[derive(Clone, Debug)] +pub struct ProgramCommandBlock { + pub client: Entity, + pub position: BlockPos, + pub command: Box, + pub mode: CommandBlockMode, + pub track_output: bool, + pub conditional: bool, + pub automatic: bool, +} + +#[derive(Clone, Debug)] +pub struct ProgramCommandBlockMinecart { + pub client: Entity, + pub entity_id: i32, + pub command: Box, + pub track_output: bool, +} + +#[derive(Clone, Debug)] +pub struct SetCreativeModeSlot { + pub client: Entity, + pub slot: i16, + pub clicked_item: Option, +} + +#[derive(Clone, Debug)] +pub struct ProgramJigsawBlock { + pub client: Entity, + pub position: BlockPos, + pub name: Ident>, + pub target: Ident>, + pub pool: Ident>, + pub final_state: Box, + pub joint_type: Box, +} + +#[derive(Clone, Debug)] +pub struct ProgramStructureBlock { + pub client: Entity, + pub position: BlockPos, + pub action: StructureBlockAction, + pub mode: StructureBlockMode, + pub name: Box, + pub offset_xyz: [i8; 3], + pub size_xyz: [i8; 3], + pub mirror: StructureBlockMirror, + pub rotation: StructureBlockRotation, + pub metadata: Box, + pub integrity: f32, + pub seed: i64, + pub flags: StructureBlockFlags, +} + +#[derive(Clone, Debug)] +pub struct UpdateSign { + pub client: Entity, + pub position: BlockPos, + pub lines: [Box; 4], +} + +#[derive(Clone, Debug)] +pub struct SwingArm { + pub client: Entity, + pub hand: Hand, +} + +#[derive(Clone, Debug)] +pub struct TeleportToEntity { + pub client: Entity, + pub target: Uuid, +} + +#[derive(Clone, Debug)] +pub struct UseItemOnBlock { + pub client: Entity, + /// The hand that was used + pub hand: Hand, + /// The location of the block that was interacted with + pub position: BlockPos, + /// The face of the block that was clicked + pub face: BlockFace, + /// The position inside of the block that was clicked on + pub cursor_pos: Vec3, + /// Whether or not the player's head is inside a block + pub head_inside_block: bool, + /// Sequence number for synchronization + pub sequence: i32, +} + +#[derive(Clone, Debug)] +pub struct UseItem { + pub client: Entity, + pub hand: Hand, + pub sequence: i32, +} + +macro_rules! events { + ( + $( + $group_number:tt { + $($name:ident)* + } + )* + ) => { + /// Inserts [`Events`] resources into the world for each client event. + pub(crate) fn register_client_events(world: &mut World) { + $( + $( + world.insert_resource(Events::<$name>::default()); + )* + )* + } + + paste! { + fn update_all_event_buffers(events: &mut ClientEvents) { + $( + let group = &mut events. $group_number; + $( + group.[< $name:snake >].update(); + )* + )* + } + + pub(crate) type ClientEvents<'w, 's> = ( + $( + [< Group $group_number >]<'w, 's>, + )* + ); + + $( + #[derive(SystemParam)] + pub(crate) struct [< Group $group_number >]<'w, 's> { + $( + [< $name:snake >]: ResMut<'w, Events<$name>>, + )* + #[system_param(ignore)] + _marker: std::marker::PhantomData<&'s ()>, } + )* + } + } +} - let got = p.teleport_id.0 as u32; - let expected = client - .teleport_id_counter - .wrapping_sub(client.pending_teleports); +// Events are grouped to get around the 16 system parameter maximum. +events! { + 0 { + QueryBlockEntity + ChangeDifficulty + MessageAcknowledgment + ChatCommand + ChatMessage + ChatPreview + PerformRespawn + RequestStats + UpdateSettings + CommandSuggestionsRequest + ClickContainerButton + ClickContainer + CloseContainer + PluginMessage + EditBook + QueryEntityTag + } + 1 { + InteractWithEntity + JigsawGenerate + LockDifficulty + SetPlayerPosition + SetPlayerPositionAndRotation + SetPlayerRotation + SetPlayerOnGround + MoveVehicle + MovePlayer + StartSneaking + StopSneaking + LeaveBed + StartSprinting + StopSprinting + StartJumpWithHorse + StopJumpWithHorse + } + 2 { + OpenHorseInventory + StartFlyingWithElytra + PaddleBoat + PickItem + PlaceRecipe + StopFlying + StartFlying + StartDigging + CancelDigging + FinishDigging + DropItem + DropItemStack + UpdateHeldItemState + SwapItemInHand + PlayerInput + Pong + } + 3 { + PlayerSession + ChangeRecipeBookSettings + SetSeenRecipe + RenameItem + ResourcePackStatusChange + OpenAdvancementTab + CloseAdvancementScreen + SelectTrade + SetBeaconEffect + SetHeldItem + ProgramCommandBlock + ProgramCommandBlockMinecart + SetCreativeModeSlot + } + 4 { + ProgramJigsawBlock + ProgramStructureBlock + UpdateSign + SwingArm + TeleportToEntity + UseItemOnBlock + UseItem + } +} - if got == expected { - client.pending_teleports -= 1; - } else { - bail!("unexpected teleport ID (expected {expected}, got {got}"); - } +pub(crate) fn event_loop_run_criteria( + mut clients: Query<(Entity, &mut Client)>, + mut clients_to_check: Local>, + mut events: ClientEvents, +) -> ShouldRun { + if clients_to_check.is_empty() { + // First run of the criteria. Prepare packets. + update_all_event_buffers(&mut events); + + for (entity, client) in &mut clients { + let client = client.into_inner(); + + let Ok(bytes) = client.conn.try_recv() else { + // Client is disconnected. + client.is_disconnected = true; + continue; + }; + + if bytes.is_empty() { + // No data was received. continue; } - C2sPlayPacket::QueryBlockEntityTag(p) => ClientEvent::QueryBlockEntity { + + client.dec.queue_bytes(bytes); + + match handle_one_packet(client, entity, &mut events) { + Ok(had_packet) => { + if had_packet { + // We decoded one packet, but there might be more. + clients_to_check.push(entity); + } + } + Err(e) => { + // TODO: validate packets in separate systems. + warn!( + username = %client.username, + uuid = %client.uuid, + ip = %client.ip, + "failed to dispatch events: {e:#}" + ); + client.is_disconnected = true; + } + } + } + } else { + // Continue to filter the list of clients we need to check until there are none + // left. + clients_to_check.retain(|&entity| { + let Ok((_, mut client)) = clients.get_mut(entity) else { + // Client was deleted during the last run of the stage. + return false; + }; + + match handle_one_packet(&mut client, entity, &mut events) { + Ok(had_packet) => had_packet, + Err(e) => { + // TODO: validate packets in separate systems. + warn!( + username = %client.username, + uuid = %client.uuid, + ip = %client.ip, + "failed to dispatch events: {e:#}" + ); + client.is_disconnected = true; + + false + } + } + }); + } + + if clients_to_check.is_empty() { + ShouldRun::No + } else { + ShouldRun::YesAndCheckAgain + } +} + +fn handle_one_packet( + client: &mut Client, + entity: Entity, + events: &mut ClientEvents, +) -> anyhow::Result { + let Some(pkt) = client.dec.try_next_packet::()? else { + // No packets to decode. + return Ok(false); + }; + + match pkt { + C2sPlayPacket::ConfirmTeleport(p) => { + if client.pending_teleports == 0 { + bail!("unexpected teleport confirmation"); + } + + let got = p.teleport_id.0 as u32; + let expected = client + .teleport_id_counter + .wrapping_sub(client.pending_teleports); + + if got == expected { + client.pending_teleports -= 1; + } else { + bail!("unexpected teleport ID (expected {expected}, got {got}"); + } + } + C2sPlayPacket::QueryBlockEntityTag(p) => { + events.0.query_block_entity.send(QueryBlockEntity { + client: entity, position: p.position, transaction_id: p.transaction_id.0, - }, - C2sPlayPacket::ChangeDifficulty(p) => ClientEvent::ChangeDifficulty(p.new_difficulty), - C2sPlayPacket::MessageAcknowledgmentC2s(p) => ClientEvent::MessageAcknowledgment { + }); + } + C2sPlayPacket::ChangeDifficulty(p) => { + events.0.change_difficulty.send(ChangeDifficulty { + client: entity, + difficulty: p.new_difficulty, + }); + } + C2sPlayPacket::MessageAcknowledgmentC2s(p) => { + events.0.message_acknowledgment.send(MessageAcknowledgment { + client: entity, message_count: p.message_count.0, - }, - C2sPlayPacket::ChatCommand(p) => ClientEvent::ChatCommand { + }); + } + C2sPlayPacket::ChatCommand(p) => { + events.0.chat_command.send(ChatCommand { + client: entity, command: p.command.into(), timestamp: p.timestamp, - }, - C2sPlayPacket::ChatMessage(p) => ClientEvent::ChatMessage { + }); + } + C2sPlayPacket::ChatMessage(p) => { + events.0.chat_message.send(ChatMessage { + client: entity, message: p.message.into(), timestamp: p.timestamp, - }, - C2sPlayPacket::ClientCommand(p) => match p { - ClientCommand::PerformRespawn => ClientEvent::PerformRespawn, - ClientCommand::RequestStats => ClientEvent::RequestStats, - }, - C2sPlayPacket::ClientInformation(p) => ClientEvent::UpdateSettings { + }); + } + C2sPlayPacket::ClientCommand(p) => match p { + ClientCommand::PerformRespawn => events + .0 + .perform_respawn + .send(PerformRespawn { client: entity }), + ClientCommand::RequestStats => { + events.0.request_stats.send(RequestStats { client: entity }) + } + }, + C2sPlayPacket::ClientInformation(p) => { + events.0.update_settings.send(UpdateSettings { + client: entity, locale: p.locale.into(), view_distance: p.view_distance, chat_mode: p.chat_mode, @@ -345,352 +839,586 @@ pub(super) fn next_event_fallible( main_hand: p.main_hand, enable_text_filtering: p.enable_text_filtering, allow_server_listings: p.allow_server_listings, - }, - C2sPlayPacket::CommandSuggestionsRequest(p) => ClientEvent::CommandSuggestionsRequest { - transaction_id: p.transaction_id.0, - text: p.text.into(), - }, - C2sPlayPacket::ClickContainerButton(p) => ClientEvent::ClickContainerButton { + }); + } + C2sPlayPacket::CommandSuggestionsRequest(p) => { + events + .0 + .command_suggestions_request + .send(CommandSuggestionsRequest { + client: entity, + transaction_id: p.transaction_id.0, + text: p.text.into(), + }); + } + C2sPlayPacket::ClickContainerButton(p) => { + events.0.click_container_button.send(ClickContainerButton { + client: entity, window_id: p.window_id, button_id: p.button_id, - }, - C2sPlayPacket::ClickContainer(p) => { - // TODO: check that the slot modifications are legal. - // TODO: update cursor item. - - for (idx, item) in &p.slots { - // TODO: check bounds on indices. - client.slots[*idx as usize] = item.clone(); - } - - ClientEvent::ClickContainer { - window_id: p.window_id, - state_id: p.state_id.0, - slot_id: p.slot_idx, - button: p.button, - mode: p.mode, - slot_changes: p.slots, - carried_item: p.carried_item, - } - } - C2sPlayPacket::CloseContainerC2s(p) => { - if client.window_id == p.window_id as u8 { - client.set_close_inventory(); - } - ClientEvent::CloseContainer { - window_id: p.window_id, - } - } - C2sPlayPacket::PluginMessageC2s(p) => ClientEvent::PluginMessage { + }); + } + C2sPlayPacket::ClickContainer(p) => { + events.0.click_container.send(ClickContainer { + client: entity, + window_id: p.window_id, + state_id: p.state_id.0, + slot_id: p.slot_idx, + button: p.button, + mode: p.mode, + slot_changes: p.slots, + carried_item: p.carried_item, + }); + } + C2sPlayPacket::CloseContainerC2s(p) => { + events.0.close_container.send(CloseContainer { + client: entity, + window_id: p.window_id, + }); + } + C2sPlayPacket::PluginMessageC2s(p) => { + events.0.plugin_message.send(PluginMessage { + client: entity, channel: p.channel.into(), data: p.data.0.into(), - }, - C2sPlayPacket::EditBook(p) => ClientEvent::EditBook { + }); + } + C2sPlayPacket::EditBook(p) => { + events.0.edit_book.send(EditBook { slot: p.slot.0, - entries: p.entries.into_iter().map(From::from).collect(), - title: p.title.map(From::from), - }, - C2sPlayPacket::QueryEntityTag(p) => ClientEvent::QueryEntity { + entries: p.entries.into_iter().map(Into::into).collect(), + title: p.title.map(Box::from), + }); + } + C2sPlayPacket::QueryEntityTag(p) => { + events.0.query_entity_tag.send(QueryEntityTag { + client: entity, transaction_id: p.transaction_id.0, entity_id: p.entity_id.0, - }, - C2sPlayPacket::Interact(p) => ClientEvent::InteractWithEntity { + }); + } + C2sPlayPacket::Interact(p) => { + events.1.interact_with_entity.send(InteractWithEntity { + client: entity, entity_id: p.entity_id.0, sneaking: p.sneaking, interact: p.interact, - }, - C2sPlayPacket::JigsawGenerate(p) => ClientEvent::JigsawGenerate { + }); + } + C2sPlayPacket::JigsawGenerate(p) => { + events.1.jigsaw_generate.send(JigsawGenerate { + client: entity, position: p.position, levels: p.levels.0, keep_jigsaws: p.keep_jigsaws, - }, - C2sPlayPacket::KeepAliveC2s(p) => { - if client.bits.got_keepalive() { - bail!("unexpected keepalive"); - } else if p.id != client.last_keepalive_id { - bail!( - "keepalive IDs don't match (expected {}, got {})", - client.last_keepalive_id, - p.id - ); - } else { - client.bits.set_got_keepalive(true); - } - - continue; + }); + } + C2sPlayPacket::KeepAliveC2s(p) => { + if client.got_keepalive { + bail!("unexpected keepalive"); + } else if p.id != client.last_keepalive_id { + bail!( + "keepalive IDs don't match (expected {}, got {})", + client.last_keepalive_id, + p.id + ); + } else { + client.got_keepalive = true; } - C2sPlayPacket::LockDifficulty(p) => ClientEvent::LockDifficulty(p.locked), - C2sPlayPacket::SetPlayerPosition(p) => { - if client.pending_teleports != 0 { - continue; - } - - client.position = p.position.into(); - - ClientEvent::SetPlayerPosition { - position: p.position, - on_ground: p.on_ground, - } + } + C2sPlayPacket::LockDifficulty(p) => { + events.1.lock_difficulty.send(LockDifficulty { + client: entity, + locked: p.locked, + }); + } + C2sPlayPacket::SetPlayerPosition(p) => { + if client.pending_teleports != 0 { + return Ok(false); } - C2sPlayPacket::SetPlayerPositionAndRotation(p) => { - if client.pending_teleports != 0 { - continue; - } - client.position = p.position.into(); - client.yaw = p.yaw; - client.pitch = p.pitch; + events.1.set_player_position.send(SetPlayerPosition { + client: entity, + position: p.position.into(), + on_ground: p.on_ground, + }); - ClientEvent::SetPlayerPositionAndRotation { - position: p.position, + events.1.move_player.send(MovePlayer { + client: entity, + old_position: client.position, + position: p.position.into(), + old_yaw: client.yaw, + yaw: client.yaw, + old_pitch: client.pitch, + pitch: client.pitch, + old_on_ground: client.on_ground, + on_ground: client.on_ground, + }); + + client.position = p.position.into(); + client.on_ground = p.on_ground; + } + C2sPlayPacket::SetPlayerPositionAndRotation(p) => { + if client.pending_teleports != 0 { + return Ok(false); + } + + events + .1 + .set_player_position_and_rotation + .send(SetPlayerPositionAndRotation { + client: entity, + position: p.position.into(), yaw: p.yaw, pitch: p.pitch, on_ground: p.on_ground, - } + }); + + events.1.move_player.send(MovePlayer { + client: entity, + old_position: client.position, + position: p.position.into(), + old_yaw: client.yaw, + yaw: p.yaw, + old_pitch: client.pitch, + pitch: p.pitch, + old_on_ground: client.on_ground, + on_ground: p.on_ground, + }); + + client.position = p.position.into(); + client.yaw = p.yaw; + client.pitch = p.pitch; + client.on_ground = p.on_ground; + } + C2sPlayPacket::SetPlayerRotation(p) => { + if client.pending_teleports != 0 { + return Ok(false); } - C2sPlayPacket::SetPlayerRotation(p) => { - if client.pending_teleports != 0 { - continue; - } - client.yaw = p.yaw; - client.pitch = p.pitch; + events.1.set_player_rotation.send(SetPlayerRotation { + client: entity, + yaw: p.yaw, + pitch: p.pitch, + on_ground: p.on_ground, + }); - ClientEvent::SetPlayerRotation { - yaw: p.yaw, - pitch: p.pitch, - on_ground: false, - } + events.1.move_player.send(MovePlayer { + client: entity, + old_position: client.position, + position: client.position, + old_yaw: client.yaw, + yaw: p.yaw, + old_pitch: client.pitch, + pitch: p.pitch, + old_on_ground: client.on_ground, + on_ground: p.on_ground, + }); + + client.yaw = p.yaw; + client.pitch = p.pitch; + client.on_ground = p.on_ground; + } + C2sPlayPacket::SetPlayerOnGround(p) => { + if client.pending_teleports != 0 { + return Ok(false); } - C2sPlayPacket::SetPlayerOnGround(p) => { - if client.pending_teleports != 0 { - continue; - } - ClientEvent::SetPlayerOnGround(p.on_ground) + events.1.set_player_on_ground.send(SetPlayerOnGround { + client: entity, + on_ground: p.on_ground, + }); + + events.1.move_player.send(MovePlayer { + client: entity, + old_position: client.position, + position: client.position, + old_yaw: client.yaw, + yaw: client.yaw, + old_pitch: client.pitch, + pitch: client.pitch, + old_on_ground: client.on_ground, + on_ground: p.on_ground, + }); + + client.on_ground = p.on_ground; + } + C2sPlayPacket::MoveVehicleC2s(p) => { + if client.pending_teleports != 0 { + return Ok(false); } - C2sPlayPacket::MoveVehicleC2s(p) => { - if client.pending_teleports != 0 { - continue; - } - client.position = p.position.into(); - client.yaw = p.yaw; - client.pitch = p.pitch; + events.1.move_vehicle.send(MoveVehicle { + client: entity, + position: p.position.into(), + yaw: p.yaw, + pitch: p.pitch, + }); - ClientEvent::MoveVehicle { - position: p.position, - yaw: p.yaw, - pitch: p.pitch, - } - } - C2sPlayPacket::PlayerCommand(p) => match p.action_id { - Action::StartSneaking => ClientEvent::StartSneaking, - Action::StopSneaking => ClientEvent::StopSneaking, - Action::LeaveBed => ClientEvent::LeaveBed, - Action::StartSprinting => ClientEvent::StartSprinting, - Action::StopSprinting => ClientEvent::StopSprinting, - Action::StartJumpWithHorse => ClientEvent::StartJumpWithHorse { - jump_boost: p.jump_boost.0.clamp(0, 100) as u8, - }, - Action::StopJumpWithHorse => ClientEvent::StopJumpWithHorse, - Action::OpenHorseInventory => ClientEvent::OpenHorseInventory, - Action::StartFlyingWithElytra => ClientEvent::StartFlyingWithElytra, - }, - C2sPlayPacket::PaddleBoat(p) => ClientEvent::PaddleBoat { + events.1.move_player.send(MovePlayer { + client: entity, + old_position: client.position, + position: p.position.into(), + old_yaw: client.yaw, + yaw: p.yaw, + old_pitch: client.pitch, + pitch: p.pitch, + old_on_ground: client.on_ground, + on_ground: client.on_ground, + }); + + client.position = p.position.into(); + client.yaw = p.yaw; + client.pitch = p.pitch; + } + C2sPlayPacket::PlayerCommand(p) => match p.action_id { + Action::StartSneaking => events + .1 + .start_sneaking + .send(StartSneaking { client: entity }), + Action::StopSneaking => events.1.stop_sneaking.send(StopSneaking { client: entity }), + Action::LeaveBed => events.1.leave_bed.send(LeaveBed { client: entity }), + Action::StartSprinting => events + .1 + .start_sprinting + .send(StartSprinting { client: entity }), + Action::StopSprinting => events + .1 + .stop_sprinting + .send(StopSprinting { client: entity }), + Action::StartJumpWithHorse => events.1.start_jump_with_horse.send(StartJumpWithHorse { + client: entity, + jump_boost: p.jump_boost.0 as u8, + }), + Action::StopJumpWithHorse => events + .1 + .stop_jump_with_horse + .send(StopJumpWithHorse { client: entity }), + Action::OpenHorseInventory => events + .2 + .open_horse_inventory + .send(OpenHorseInventory { client: entity }), + Action::StartFlyingWithElytra => events + .2 + .start_flying_with_elytra + .send(StartFlyingWithElytra { client: entity }), + }, + C2sPlayPacket::PaddleBoat(p) => { + events.2.paddle_boat.send(PaddleBoat { + client: entity, left_paddle_turning: p.left_paddle_turning, right_paddle_turning: p.right_paddle_turning, - }, - C2sPlayPacket::PickItem(p) => ClientEvent::PickItem { + }); + } + C2sPlayPacket::PickItem(p) => { + events.2.pick_item.send(PickItem { + client: entity, slot_to_use: p.slot_to_use.0, - }, - C2sPlayPacket::PlaceRecipe(p) => ClientEvent::PlaceRecipe { + }); + } + C2sPlayPacket::PlaceRecipe(p) => { + events.2.place_recipe.send(PlaceRecipe { + client: entity, window_id: p.window_id, recipe: p.recipe.into(), make_all: p.make_all, - }, - C2sPlayPacket::PlayerAbilitiesC2s(p) => match p { - PlayerAbilitiesC2s::StopFlying => ClientEvent::StopFlying, - PlayerAbilitiesC2s::StartFlying => ClientEvent::StartFlying, - }, - C2sPlayPacket::PlayerAction(p) => { - if p.sequence.0 != 0 { - client.block_change_sequence = - cmp::max(p.sequence.0, client.block_change_sequence); - } - - match p.status { - DiggingStatus::StartedDigging => ClientEvent::StartDigging { - position: p.position, - face: p.face, - sequence: p.sequence.0, - }, - DiggingStatus::CancelledDigging => ClientEvent::CancelDigging { - position: p.position, - face: p.face, - sequence: p.sequence.0, - }, - DiggingStatus::FinishedDigging => ClientEvent::FinishDigging { - position: p.position, - face: p.face, - sequence: p.sequence.0, - }, - DiggingStatus::DropItemStack => ClientEvent::DropItemStack, - DiggingStatus::DropItem => ClientEvent::DropItem, - DiggingStatus::UpdateHeldItemState => ClientEvent::UpdateHeldItemState, - DiggingStatus::SwapItemInHand => ClientEvent::SwapItemInHand, - } + }); + } + C2sPlayPacket::PlayerAbilitiesC2s(p) => match p { + PlayerAbilitiesC2s::StopFlying => { + events.2.stop_flying.send(StopFlying { client: entity }) } - C2sPlayPacket::PlayerInput(p) => ClientEvent::PlayerInput { + PlayerAbilitiesC2s::StartFlying => { + events.2.start_flying.send(StartFlying { client: entity }) + } + }, + C2sPlayPacket::PlayerAction(p) => { + if p.sequence.0 != 0 { + client.block_change_sequence = cmp::max(p.sequence.0, client.block_change_sequence); + } + + match p.status { + DiggingStatus::StartedDigging => events.2.start_digging.send(StartDigging { + client: entity, + position: p.position, + face: p.face, + sequence: p.sequence.0, + }), + DiggingStatus::CancelledDigging => events.2.cancel_digging.send(CancelDigging { + client: entity, + position: p.position, + face: p.face, + sequence: p.sequence.0, + }), + DiggingStatus::FinishedDigging => events.2.finish_digging.send(FinishDigging { + client: entity, + position: p.position, + face: p.face, + sequence: p.sequence.0, + }), + DiggingStatus::DropItemStack => events + .2 + .drop_item_stack + .send(DropItemStack { client: entity }), + DiggingStatus::DropItem => events.2.drop_item.send(DropItem { client: entity }), + DiggingStatus::UpdateHeldItemState => events + .2 + .update_held_item_state + .send(UpdateHeldItemState { client: entity }), + DiggingStatus::SwapItemInHand => events + .2 + .swap_item_in_hand + .send(SwapItemInHand { client: entity }), + } + } + C2sPlayPacket::PlayerInput(p) => { + events.2.player_input.send(PlayerInput { + client: entity, sideways: p.sideways, forward: p.forward, jump: p.flags.jump(), unmount: p.flags.unmount(), - }, - C2sPlayPacket::PongPlay(p) => ClientEvent::Pong { id: p.id }, - C2sPlayPacket::PlayerSession(p) => ClientEvent::PlayerSession { + }); + } + C2sPlayPacket::PongPlay(p) => { + events.2.pong.send(Pong { + client: entity, + id: p.id, + }); + } + C2sPlayPacket::PlayerSession(p) => { + events.3.player_session.send(PlayerSession { + client: entity, session_id: p.session_id, expires_at: p.expires_at, public_key_data: p.public_key_data.into(), key_signature: p.key_signature.into(), - }, - C2sPlayPacket::ChangeRecipeBookSettings(p) => ClientEvent::ChangeRecipeBookSettings { - book_id: p.book_id, - book_open: p.book_open, - filter_active: p.filter_active, - }, - C2sPlayPacket::SetSeenRecipe(p) => ClientEvent::SetSeenRecipe { + }); + } + C2sPlayPacket::ChangeRecipeBookSettings(p) => { + events + .3 + .change_recipe_book_settings + .send(ChangeRecipeBookSettings { + client: entity, + book_id: p.book_id, + book_open: p.book_open, + filter_active: p.filter_active, + }); + } + C2sPlayPacket::SetSeenRecipe(p) => { + events.3.set_seen_recipe.send(SetSeenRecipe { + client: entity, recipe_id: p.recipe_id.into(), - }, - C2sPlayPacket::RenameItem(p) => ClientEvent::RenameItem { + }); + } + C2sPlayPacket::RenameItem(p) => { + events.3.rename_item.send(RenameItem { + client: entity, name: p.item_name.into(), - }, - C2sPlayPacket::ResourcePackC2s(p) => match p { - ResourcePackC2s::SuccessfullyLoaded => ClientEvent::ResourcePackLoaded, - ResourcePackC2s::Declined => ClientEvent::ResourcePackDeclined, - ResourcePackC2s::FailedDownload => ClientEvent::ResourcePackFailedDownload, - ResourcePackC2s::Accepted => ClientEvent::ResourcePackAccepted, - }, - C2sPlayPacket::SeenAdvancements(p) => match p { - SeenAdvancements::OpenedTab { tab_id } => ClientEvent::OpenAdvancementTab { + }); + } + C2sPlayPacket::ResourcePackC2s(p) => { + events + .3 + .resource_pack_status_change + .send(ResourcePackStatusChange { + client: entity, + status: p.into(), + }) + } + C2sPlayPacket::SeenAdvancements(p) => match p { + SeenAdvancements::OpenedTab { tab_id } => { + events.3.open_advancement_tab.send(OpenAdvancementTab { + client: entity, tab_id: tab_id.into(), - }, - SeenAdvancements::ClosedScreen => ClientEvent::CloseAdvancementScreen, - }, - C2sPlayPacket::SelectTrade(p) => ClientEvent::SelectTrade { + }) + } + SeenAdvancements::ClosedScreen => events + .3 + .close_advancement_screen + .send(CloseAdvancementScreen { client: entity }), + }, + C2sPlayPacket::SelectTrade(p) => { + events.3.select_trade.send(SelectTrade { + client: entity, slot: p.selected_slot.0, - }, - C2sPlayPacket::SetBeaconEffect(p) => ClientEvent::SetBeaconEffect { + }); + } + C2sPlayPacket::SetBeaconEffect(p) => { + events.3.set_beacon_effect.send(SetBeaconEffect { + client: entity, primary_effect: p.primary_effect.map(|i| i.0), secondary_effect: p.secondary_effect.map(|i| i.0), - }, - C2sPlayPacket::SetHeldItemC2s(p) => ClientEvent::SetHeldItem { slot: p.slot }, - C2sPlayPacket::ProgramCommandBlock(p) => ClientEvent::ProgramCommandBlock { + }); + } + C2sPlayPacket::SetHeldItemC2s(p) => events.3.set_held_item.send(SetHeldItem { + client: entity, + slot: p.slot, + }), + C2sPlayPacket::ProgramCommandBlock(p) => { + events.3.program_command_block.send(ProgramCommandBlock { + client: entity, position: p.position, command: p.command.into(), mode: p.mode, track_output: p.flags.track_output(), conditional: p.flags.conditional(), automatic: p.flags.automatic(), - }, - C2sPlayPacket::ProgramCommandBlockMinecart(p) => { - ClientEvent::ProgramCommandBlockMinecart { + }); + } + C2sPlayPacket::ProgramCommandBlockMinecart(p) => { + events + .3 + .program_command_block_minecart + .send(ProgramCommandBlockMinecart { + client: entity, entity_id: p.entity_id.0, command: p.command.into(), track_output: p.track_output, - } - } - C2sPlayPacket::SetCreativeModeSlot(p) => ClientEvent::SetCreativeModeSlot { + }); + } + C2sPlayPacket::SetCreativeModeSlot(p) => { + events.3.set_creative_mode_slot.send(SetCreativeModeSlot { + client: entity, slot: p.slot, clicked_item: p.clicked_item, - }, - C2sPlayPacket::ProgramJigsawBlock(p) => ClientEvent::ProgramJigsawBlock { + }); + } + C2sPlayPacket::ProgramJigsawBlock(p) => { + events.4.program_jigsaw_block.send(ProgramJigsawBlock { + client: entity, position: p.position, name: p.name.into(), target: p.target.into(), pool: p.pool.into(), final_state: p.final_state.into(), joint_type: p.joint_type.into(), - }, - C2sPlayPacket::ProgramStructureBlock(p) => ClientEvent::ProgramStructureBlock { - position: p.position, - action: p.action, - mode: p.mode, - name: p.name.into(), - offset_xyz: p.offset_xyz, - size_xyz: p.size_xyz, - mirror: p.mirror, - rotation: p.rotation, - metadata: p.metadata.into(), - integrity: p.integrity, - seed: p.seed, - flags: p.flags, - }, - C2sPlayPacket::UpdateSign(p) => ClientEvent::UpdateSign { - position: p.position, - lines: p.lines.map(From::from), - }, - C2sPlayPacket::SwingArm(p) => ClientEvent::SwingArm(p.hand), - C2sPlayPacket::TeleportToEntity(p) => { - ClientEvent::TeleportToEntity { target: p.target } - } - C2sPlayPacket::UseItemOn(p) => { - if p.sequence.0 != 0 { - client.block_change_sequence = - cmp::max(p.sequence.0, client.block_change_sequence); - } - - ClientEvent::UseItemOnBlock { - hand: p.hand, + }); + } + C2sPlayPacket::ProgramStructureBlock(p) => { + events + .4 + .program_structure_block + .send(ProgramStructureBlock { + client: entity, position: p.position, - face: p.face, - cursor_pos: p.cursor_pos, - head_inside_block: p.head_inside_block, - sequence: p.sequence.0, - } + action: p.action, + mode: p.mode, + name: p.name.into(), + offset_xyz: p.offset_xyz, + size_xyz: p.size_xyz, + mirror: p.mirror, + rotation: p.rotation, + metadata: p.metadata.into(), + integrity: p.integrity, + seed: p.seed.0, + flags: p.flags, + }) + } + C2sPlayPacket::UpdateSign(p) => { + events.4.update_sign.send(UpdateSign { + client: entity, + position: p.position, + lines: p.lines.map(Into::into), + }); + } + C2sPlayPacket::SwingArm(p) => { + events.4.swing_arm.send(SwingArm { + client: entity, + hand: p.hand, + }); + } + C2sPlayPacket::TeleportToEntity(p) => { + events.4.teleport_to_entity.send(TeleportToEntity { + client: entity, + target: p.target, + }); + } + C2sPlayPacket::UseItemOn(p) => { + if p.sequence.0 != 0 { + client.block_change_sequence = cmp::max(p.sequence.0, client.block_change_sequence); } - C2sPlayPacket::UseItem(p) => { - if p.sequence.0 != 0 { - client.block_change_sequence = - cmp::max(p.sequence.0, client.block_change_sequence); - } - ClientEvent::UseItem { - hand: p.hand, - sequence: p.sequence.0, - } + events.4.use_item_on_block.send(UseItemOnBlock { + client: entity, + hand: p.hand, + position: p.position, + face: p.face, + cursor_pos: p.cursor_pos.into(), + head_inside_block: false, + sequence: 0, + }) + } + C2sPlayPacket::UseItem(p) => { + if p.sequence.0 != 0 { + client.block_change_sequence = cmp::max(p.sequence.0, client.block_change_sequence); } - })); + + events.4.use_item.send(UseItem { + client: entity, + hand: p.hand, + sequence: p.sequence.0, + }); + } } + + Ok(true) } -impl ClientEvent { - /// Takes a client event, a client, and an entity representing the client - /// and expresses the event in a reasonable way. - /// - /// For instance, movement events are expressed by changing the entity's - /// position/rotation to match the received movement, crouching makes the - /// entity crouch, etc. - /// - /// This function's primary purpose is to reduce boilerplate code in the - /// examples, but it can be used as a quick way to get started in your own - /// code. The precise behavior of this function is left unspecified and - /// is subject to change. - pub fn handle_default(&self, client: &mut Client, entity: &mut Entity) { - match self { - ClientEvent::RequestStats => { - // TODO: award empty statistics - } - ClientEvent::UpdateSettings { - view_distance, - displayed_skin_parts, - main_hand, - .. - } => { - client.set_view_distance(*view_distance); +/// The default event handler system which handles client events in a +/// reasonable default way. +/// +/// For instance, movement events are handled by changing the entity's +/// position/rotation to match the received movement, crouching makes the +/// entity crouch, etc. +/// +/// This system's primary purpose is to reduce boilerplate code in the +/// examples, but it can be used as a quick way to get started in your own +/// code. The precise behavior of this system is left unspecified and +/// is subject to change. +/// +/// This system must be scheduled to run in the +/// [`EventLoop`](crate::server::EventLoop) stage. Otherwise, it may not +/// function correctly. +#[allow(clippy::too_many_arguments)] +pub fn default_event_handler( + mut clients: Query<(&mut Client, Option<&mut McEntity>)>, + mut update_settings: EventReader, + mut move_player: EventReader, + mut start_sneaking: EventReader, + mut stop_sneaking: EventReader, + mut start_sprinting: EventReader, + mut stop_sprinting: EventReader, + mut swing_arm: EventReader, +) { + for UpdateSettings { + client, + view_distance, + displayed_skin_parts, + main_hand, + .. + } in update_settings.iter() + { + let Ok((mut client, entity)) = clients.get_mut(*client) else { + continue + }; - let player = client.player_mut(); + client.set_view_distance(*view_distance); + let player = client.player_mut(); + + player.set_cape(displayed_skin_parts.cape()); + player.set_jacket(displayed_skin_parts.jacket()); + player.set_left_sleeve(displayed_skin_parts.left_sleeve()); + player.set_right_sleeve(displayed_skin_parts.right_sleeve()); + player.set_left_pants_leg(displayed_skin_parts.left_pants_leg()); + player.set_right_pants_leg(displayed_skin_parts.right_pants_leg()); + player.set_hat(displayed_skin_parts.hat()); + player.set_main_arm(*main_hand as u8); + + if let Some(mut entity) = entity { + if let TrackedData::Player(player) = entity.data_mut() { player.set_cape(displayed_skin_parts.cape()); player.set_jacket(displayed_skin_parts.jacket()); player.set_left_sleeve(displayed_skin_parts.left_sleeve()); @@ -699,89 +1427,80 @@ impl ClientEvent { player.set_right_pants_leg(displayed_skin_parts.right_pants_leg()); player.set_hat(displayed_skin_parts.hat()); player.set_main_arm(*main_hand as u8); + } + } + } - if let TrackedData::Player(player) = entity.data_mut() { - player.set_cape(displayed_skin_parts.cape()); - player.set_jacket(displayed_skin_parts.jacket()); - player.set_left_sleeve(displayed_skin_parts.left_sleeve()); - player.set_right_sleeve(displayed_skin_parts.right_sleeve()); - player.set_left_pants_leg(displayed_skin_parts.left_pants_leg()); - player.set_right_pants_leg(displayed_skin_parts.right_pants_leg()); - player.set_hat(displayed_skin_parts.hat()); - player.set_main_arm(*main_hand as u8); - } - } - ClientEvent::CommandSuggestionsRequest { .. } => {} - ClientEvent::SetPlayerPosition { - position, - on_ground, - } => { - entity.set_position(*position); - entity.set_on_ground(*on_ground); - } - ClientEvent::SetPlayerPositionAndRotation { - position, - yaw, - pitch, - on_ground, - } => { - entity.set_position(*position); - entity.set_yaw(*yaw); - entity.set_head_yaw(*yaw); - entity.set_pitch(*pitch); - entity.set_on_ground(*on_ground); - } - ClientEvent::SetPlayerRotation { - yaw, - pitch, - on_ground, - } => { - entity.set_yaw(*yaw); - entity.set_head_yaw(*yaw); - entity.set_pitch(*pitch); - entity.set_on_ground(*on_ground); - } - ClientEvent::SetPlayerOnGround(on_ground) => entity.set_on_ground(*on_ground), - ClientEvent::MoveVehicle { - position, - yaw, - pitch, - } => { - entity.set_position(*position); - entity.set_yaw(*yaw); - entity.set_pitch(*pitch); - } - ClientEvent::StartSneaking => { - if let TrackedData::Player(player) = entity.data_mut() { - if player.get_pose() == Pose::Standing { - player.set_pose(Pose::Sneaking); - } - } - } - ClientEvent::StopSneaking => { - if let TrackedData::Player(player) = entity.data_mut() { - if player.get_pose() == Pose::Sneaking { - player.set_pose(Pose::Standing); - } - } - } - ClientEvent::StartSprinting => { - if let TrackedData::Player(player) = entity.data_mut() { - player.set_sprinting(true); - } - } - ClientEvent::StopSprinting => { - if let TrackedData::Player(player) = entity.data_mut() { - player.set_sprinting(false); - } - } - ClientEvent::SwingArm(hand) => { - entity.push_event(match hand { - Hand::Main => EntityEvent::SwingMainHand, - Hand::Off => EntityEvent::SwingOffHand, - }); - } - _ => {} + for MovePlayer { + client, + position, + yaw, + pitch, + on_ground, + .. + } in move_player.iter() + { + let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { + continue + }; + + entity.set_position(*position); + entity.set_yaw(*yaw); + entity.set_head_yaw(*yaw); + entity.set_pitch(*pitch); + entity.set_on_ground(*on_ground); + } + + for StartSneaking { client } in start_sneaking.iter() { + let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { + continue + }; + + if let TrackedData::Player(player) = entity.data_mut() { + player.set_pose(Pose::Sneaking); + } + } + + for StopSneaking { client } in stop_sneaking.iter() { + let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { + continue + }; + + if let TrackedData::Player(player) = entity.data_mut() { + player.set_pose(Pose::Standing); + } + } + + for StartSprinting { client } in start_sprinting.iter() { + let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { + continue + }; + + if let TrackedData::Player(player) = entity.data_mut() { + player.set_sprinting(true); + } + } + + for StopSprinting { client } in stop_sprinting.iter() { + let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { + continue + }; + + if let TrackedData::Player(player) = entity.data_mut() { + player.set_sprinting(false); + } + } + + for SwingArm { client, hand } in swing_arm.iter() { + let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { + continue + }; + + if entity.kind() == EntityKind::Player { + entity.trigger_animation(match hand { + Hand::Main => EntityAnimation::SwingMainHand, + Hand::Off => EntityAnimation::SwingOffHand, + }); } } } diff --git a/crates/valence/src/config.rs b/crates/valence/src/config.rs index 3aa823d..6a751c6 100644 --- a/crates/valence/src/config.rs +++ b/crates/valence/src/config.rs @@ -1,76 +1,48 @@ -//! Configuration for the server. - -use std::borrow::Cow; use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::sync::Arc; use async_trait::async_trait; +use bevy_app::{App, Plugin}; use serde::Serialize; -use tokio::runtime::Handle as TokioHandle; +use tokio::runtime::Handle; +use tracing::error; use uuid::Uuid; -use valence_protocol::text::Text; -use valence_protocol::username::Username; -use valence_protocol::MAX_PACKET_SIZE; +use valence_protocol::{Text, Username}; use crate::biome::Biome; use crate::dimension::Dimension; -use crate::server::{NewClientData, Server, SharedServer}; -use crate::{Ticks, STANDARD_TPS}; +use crate::server::{NewClientInfo, SharedServer}; -/// A trait for the configuration of a server. -/// -/// This trait uses the [async_trait] attribute macro. It is exported at the -/// root of this crate. async_trait will be removed once async fns in traits -/// are stabilized. -/// -/// [async_trait]: https://docs.rs/async-trait/latest/async_trait/ -#[async_trait] -#[allow(unused_variables)] -pub trait Config: Sized + Send + Sync + 'static { - /// Custom state to store with the [`Server`]. - type ServerState: Send + Sync; - /// Custom state to store with every [`Client`](crate::client::Client). - type ClientState: Default + Send + Sync; - /// Custom state to store with every [`Entity`](crate::entity::Entity). - type EntityState: Send + Sync; - /// Custom state to store with every [`World`](crate::world::World). - type WorldState: Send + Sync; - /// Custom state to store with every - /// [`LoadedChunk`](crate::chunk::LoadedChunk). - type ChunkState: Send + Sync; - /// Custom state to store with every - /// [`PlayerList`](crate::player_list::PlayerList). - type PlayerListState: Send + Sync; - /// Custom state to store with every - /// [`Inventory`](crate::inventory::Inventory). - type InventoryState: Send + Sync; - - /// Called once at startup to get the maximum number of simultaneous - /// connections allowed to the server. This includes all - /// connections, not just those past the login stage. +#[derive(Clone)] +#[non_exhaustive] +pub struct ServerPlugin { + pub callbacks: Arc, + /// The [`Handle`] to the tokio runtime the server will use. If `None` is + /// provided, the server will create its own tokio runtime at startup. + /// + /// # Default Value + /// + /// `None` + pub tokio_handle: Option, + /// The maximum number of simultaneous connections allowed to the server. + /// This includes all connections, not just those past the login stage. /// /// You will want this value to be somewhere above the maximum number of /// players, since status pings should still succeed even when the server is /// full. /// - /// # Default Implementation + /// # Default Value /// - /// Currently returns `1024`. This may change in a future version. - fn max_connections(&self) -> usize { - 1024 - } - - /// Called once at startup to get the socket address the server will - /// be bound to. + /// `1024`. This may change in a future version. + pub max_connections: usize, + /// The socket address the server will be bound to. /// - /// # Default Implementation + /// # Default Value /// - /// Returns `0.0.0.0:25565` to listen on every available network interface. - fn address(&self) -> SocketAddr { - SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 25565).into() - } - - /// Called once at startup to get the tick rate, which is the number of game - /// updates that should occur in one second. + /// `0.0.0.0:25565`, which will listen on every available network interface. + pub address: SocketAddr, + /// The ticks per second of the server. This is the number of game updates + /// that should occur in one second. /// /// On each game update (tick), the server is expected to update game logic /// and respond to packets from clients. Once this is complete, the server @@ -81,39 +53,239 @@ pub trait Config: Sized + Send + Sync + 'static { /// Note that the official Minecraft client only processes packets at 20hz, /// so there is little benefit to a tick rate higher than 20. /// - /// # Default Implementation + /// # Default Value /// - /// Returns [`STANDARD_TPS`]. - fn tick_rate(&self) -> Ticks { - STANDARD_TPS - } - - /// Called to get the connection mode option, which determines if client - /// authentication and encryption should take place and if the server - /// should get the player data from a proxy. + /// [`DEFAULT_TPS`] + pub tps: i64, + /// The connection mode. This determines if client authentication and + /// encryption should take place and if the server should get the player + /// data from a proxy. /// - /// # Default Implementation + /// # Default Value /// - /// Returns [`ConnectionMode::Online`]. - fn connection_mode(&self) -> ConnectionMode { - ConnectionMode::Online - } - - /// Obtains the compression threshold to use for compressing packets. For a + /// [`ConnectionMode::Online`] + pub connection_mode: ConnectionMode, + /// The compression threshold to use for compressing packets. For a /// compression threshold of `Some(N)`, packets with encoded lengths >= `N` - /// are compressed while all others are not. `None` disables compression. + /// are compressed while all others are not. `None` disables compression + /// completely. + /// + /// If the server is used behind a proxy on the same machine, you will + /// likely want to disable compression. + /// + /// # Default Value + /// + /// Compression is enabled with an unspecified threshold. + pub compression_threshold: Option, + /// The maximum capacity (in bytes) of the buffer used to hold incoming + /// packet data. + /// + /// A larger capacity reduces the chance that a client needs to be + /// disconnected due to the buffer being full, but increases potential + /// memory usage. + /// + /// # Default Value + /// + /// An unspecified value is used that should be adequate for most + /// situations. This default may change in future versions. + pub incoming_capacity: usize, + /// The maximum capacity (in bytes) of the buffer used to hold outgoing + /// packets. + /// + /// A larger capacity reduces the chance that a client needs to be + /// disconnected due to the buffer being full, but increases potential + /// memory usage. + /// + /// # Default Value + /// + /// An unspecified value is used that should be adequate for most + /// situations. This default may change in future versions. + pub outgoing_capacity: usize, + /// The list of [`Dimension`]s usable on the server. + /// + /// The dimensions returned by [`ServerPlugin::dimensions`] will be in the + /// same order as this `Vec`. + /// + /// The number of elements in the `Vec` must be in `1..=u16::MAX`. + /// Additionally, the documented requirements on the fields of [`Dimension`] + /// must be met. + /// + /// # Default Value + /// + /// `vec![Dimension::default()]` + pub dimensions: Arc<[Dimension]>, + /// The list of [`Biome`]s usable on the server. + /// + /// The biomes returned by [`SharedServer::biomes`] will be in the same + /// order as this `Vec`. + /// + /// The number of elements in the `Vec` must be in `1..=u16::MAX`. + /// Additionally, the documented requirements on the fields of [`Biome`] + /// must be met. + /// + /// **NOTE**: As of 1.19.2, there is a bug in the client which prevents + /// joining the game when a biome named "minecraft:plains" is not present. + /// Ensure there is a biome named "plains". + /// + /// # Default Value + /// + /// `vec![Biome::default()]`. + pub biomes: Arc<[Biome]>, +} + +impl ServerPlugin { + pub fn new(callbacks: impl Into>) -> Self { + Self { + callbacks: callbacks.into(), + tokio_handle: None, + max_connections: 1024, + address: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 25565).into(), + tps: DEFAULT_TPS, + connection_mode: ConnectionMode::Online { + // Note: Some people have problems using valence when this is enabled by default. + prevent_proxy_connections: false, + }, + compression_threshold: Some(256), + incoming_capacity: 2097152, // 2 MiB + outgoing_capacity: 8388608, // 8 MiB + dimensions: [Dimension::default()].as_slice().into(), + biomes: [Biome::default()].as_slice().into(), + } + } + + /// See [`Self::tokio_handle`]. + #[must_use] + pub fn with_tokio_handle(mut self, tokio_handle: Option) -> Self { + self.tokio_handle = tokio_handle; + self + } + + /// See [`Self::max_connections`]. + #[must_use] + pub fn with_max_connections(mut self, max_connections: usize) -> Self { + self.max_connections = max_connections; + self + } + + /// See [`Self::address`]. + #[must_use] + pub fn with_address(mut self, address: SocketAddr) -> Self { + self.address = address; + self + } + + /// See [`Self::tps`]. + #[must_use] + pub fn with_tick_rate(mut self, tick_rate: i64) -> Self { + self.tps = tick_rate; + self + } + + /// See [`Self::connection_mode`]. + #[must_use] + pub fn with_connection_mode(mut self, connection_mode: ConnectionMode) -> Self { + self.connection_mode = connection_mode; + self + } + + /// See [`Self::compression_threshold`]. + #[must_use] + pub fn with_compression_threshold(mut self, compression_threshold: Option) -> Self { + self.compression_threshold = compression_threshold; + self + } + + /// See [`Self::incoming_capacity`]. + #[must_use] + pub fn with_incoming_capacity(mut self, incoming_capacity: usize) -> Self { + self.incoming_capacity = incoming_capacity; + self + } + + /// See [`Self::outgoing_capacity`]. + #[must_use] + pub fn with_outgoing_capacity(mut self, outgoing_capacity: usize) -> Self { + self.outgoing_capacity = outgoing_capacity; + self + } + + /// See [`Self::dimensions`]. + #[must_use] + pub fn with_dimensions(mut self, dimensions: impl Into>) -> Self { + self.dimensions = dimensions.into(); + self + } + + /// See [`Self::biomes`]. + #[must_use] + pub fn with_biomes(mut self, biomes: impl Into>) -> Self { + self.biomes = biomes.into(); + self + } +} + +impl Default for ServerPlugin { + fn default() -> Self { + Self::new(A::default()) + } +} + +impl Plugin for ServerPlugin { + fn build(&self, app: &mut App) { + if let Err(e) = crate::server::build_plugin(self, app) { + error!("failed to build Valence plugin: {e:#}"); + } + } +} + +#[async_trait] +pub trait AsyncCallbacks: Send + Sync + 'static { + /// Called when the server receives a Server List Ping query. + /// Data for the response can be provided or the query can be ignored. + /// + /// This function is called from within a tokio runtime. /// /// # Default Implementation /// - /// If the connection mode is [`ConnectionMode::Online`], `Some(256)` is - /// returned. Otherwise, compression is disabled. - fn compression_threshold(&self) -> Option { - match self.connection_mode() { - ConnectionMode::Online => Some(256), - _ => None, + /// A default placeholder response is returned. + async fn server_list_ping( + &self, + shared: &SharedServer, + remote_addr: SocketAddr, + protocol_version: i32, + ) -> ServerListPing { + #![allow(unused_variables)] + ServerListPing::Respond { + online_players: 0, // TODO: get online players. + max_players: -1, + player_sample: vec![], + description: "A Valence Server".into(), + favicon_png: &[], } } + /// Called for each client after successful authentication (if online mode + /// is enabled) to determine if they can join the server. On success, a + /// new entity is spawned with the [`Client`] component. If this method + /// returns with `Err(reason)`, then the client is immediately + /// disconnected with `reason` as the displayed message. + /// + /// This method is the appropriate place to perform asynchronous + /// operations such as database queries which may take some time to + /// complete. + /// + /// This method is called from within a tokio runtime. + /// + /// # Default Implementation + /// + /// The client is allowed to join unconditionally. + /// + /// [`Client`]: crate::client::Client + async fn login(&self, shared: &SharedServer, info: &NewClientInfo) -> Result<(), Text> { + #![allow(unused_variables)] + Ok(()) + } + /// Called upon every client login to obtain the full URL to use for session /// server requests. This is done to authenticate player accounts. This /// method is not called unless [online mode] is enabled. @@ -131,233 +303,52 @@ pub trait Config: Sized + Send + Sync + 'static { /// `https://sessionserver.mojang.com/session/minecraft/hasJoined?username=&serverId=&ip=`. /// /// [online mode]: crate::config::ConnectionMode::Online - fn session_server( + async fn session_server( &self, - server: &SharedServer, + shared: &SharedServer, username: Username<&str>, auth_digest: &str, player_ip: &IpAddr, ) -> String { - if self.prevent_proxy_connections() { + if shared.connection_mode() + == (&ConnectionMode::Online { + prevent_proxy_connections: true, + }) + { format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={auth_digest}&ip={player_ip}") } else { format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={auth_digest}") } } - - /// Called from the default implementation of [`Config::session_server`] to - /// get the "prevent-proxy-connections" option, which determines if client - /// IP validation should take place. - /// - /// When `prevent_proxy_connections` is enabled, clients can no longer - /// log-in if they connected to the yggdrasil server using a different - /// IP. - /// - /// # Default Implementation - /// - /// Proxy connections are allowed. - /// - /// Returns `false`. - fn prevent_proxy_connections(&self) -> bool { - false - } - - /// Called once at startup to get the maximum capacity (in bytes) of the - /// buffer used to hold incoming packet data. - /// - /// A larger capacity reduces the chance that a client needs to be - /// disconnected due to the buffer being full, but increases potential - /// memory usage. - /// - /// # Default Implementation - /// - /// An unspecified value is returned that should be adequate for most - /// situations. This default may change in future versions. - fn incoming_capacity(&self) -> usize { - MAX_PACKET_SIZE as usize - } - - /// Called once at startup to get the maximum capacity (in bytes) of the - /// buffer used to hold outgoing packets. - /// - /// A larger capacity reduces the chance that a client needs to be - /// disconnected due to the buffer being full, but increases potential - /// memory usage. - /// - /// # Default Implementation - /// - /// An unspecified value is returned that should be adequate for most - /// situations. This default may change in future versions. - fn outgoing_capacity(&self) -> usize { - MAX_PACKET_SIZE as usize * 4 - } - - /// Called once at startup to get a handle to the tokio runtime the server - /// will use. - /// - /// If a handle is not provided, the server will create its own tokio - /// runtime. - /// - /// # Default Implementation - /// - /// Returns `None`. - fn tokio_handle(&self) -> Option { - None - } - - /// Called once at startup to get the list of [`Dimension`]s usable on the - /// server. - /// - /// The dimensions returned by [`SharedServer::dimensions`] will be in the - /// same order as the `Vec` returned by this function. - /// - /// The number of elements in the returned `Vec` must be in `1..=u16::MAX`. - /// Additionally, the documented requirements on the fields of [`Dimension`] - /// must be met. - /// - /// # Default Implementation - /// - /// Returns `vec![Dimension::default()]`. - fn dimensions(&self) -> Vec { - vec![Dimension::default()] - } - - /// Called once at startup to get the list of [`Biome`]s usable on the - /// server. - /// - /// The biomes returned by [`SharedServer::biomes`] will be in the same - /// order as the `Vec` returned by this function. - /// - /// The number of elements in the returned `Vec` must be in `1..=u16::MAX`. - /// Additionally, the documented requirements on the fields of [`Biome`] - /// must be met. - /// - /// **NOTE**: As of 1.19.2, there is a bug in the client which prevents - /// joining the game when a biome named "minecraft:plains" is not present. - /// Ensure there is a biome named "plains". - /// - /// # Default Implementation - /// - /// Returns `vec![Biome::default()]`. - fn biomes(&self) -> Vec { - vec![Biome::default()] - } - - /// Called when the server receives a Server List Ping query. - /// Data for the response can be provided or the query can be ignored. - /// - /// This method is called from within a tokio runtime. - /// - /// # Default Implementation - /// - /// The query is ignored. - async fn server_list_ping( - &self, - shared: &SharedServer, - remote_addr: SocketAddr, - protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Ignore - } - - /// Called asynchronously for each client after successful authentication - /// (if online mode is enabled) to determine if they can join - /// the server. On success, the new client is added to the server's - /// [`Clients`]. If this method returns with `Err(reason)`, then the - /// client is immediately disconnected with the given reason. - /// - /// This method is the appropriate place to perform asynchronous - /// operations such as database queries which may take some time to - /// complete. - /// - /// This method is called from within a tokio runtime. - /// - /// # Default Implementation - /// - /// The client is allowed to join unconditionally. - /// - /// [`Clients`]: crate::client::Clients - async fn login(&self, shared: &SharedServer, ncd: &NewClientData) -> Result<(), Text> { - Ok(()) - } - - /// Called after the server is created, but prior to accepting connections - /// and entering the update loop. - /// - /// This is useful for performing initialization work with a guarantee that - /// no connections to the server will be made until this function returns. - /// - /// This method is called from within a tokio runtime. - /// - /// # Default Implementation - /// - /// The default implementation does nothing. - fn init(&self, server: &mut Server) {} - - /// Called once at the beginning of every server update (also known as - /// "tick"). This is likely where the majority of your code will be. - /// - /// The frequency of ticks can be configured by [`Self::tick_rate`]. - /// - /// This method is called from within a tokio runtime. - /// - /// # Default Implementation - /// - /// The default implementation does nothing. - fn update(&self, server: &mut Server) {} } -/// The result of the [`server_list_ping`](Config::server_list_ping) callback. -#[derive(Clone, Debug)] -pub enum ServerListPing<'a> { - /// Responds to the server list ping with the given information. - Respond { - /// Displayed as the number of players on the server. - online_players: i32, - /// Displayed as the maximum number of players allowed on the server at - /// a time. - max_players: i32, - /// The list of players visible by hovering over the player count. - /// - /// Has no effect if this list is empty. - player_sample: Cow<'a, [PlayerSampleEntry<'a>]>, - /// A description of the server. - description: Text, - /// The server's icon as the bytes of a PNG image. - /// The image must be 64x64 pixels. - /// - /// No icon is used if the value is `None`. - favicon_png: Option>, - }, - /// Ignores the query and disconnects from the client. - Ignore, -} - -/// Represents an individual entry in the player sample. -#[derive(Clone, Debug, Serialize)] -pub struct PlayerSampleEntry<'a> { - /// The name of the player. - /// - /// This string can contain - /// [legacy formatting codes](https://minecraft.fandom.com/wiki/Formatting_codes). - pub name: Cow<'a, str>, - /// The player UUID. - pub id: Uuid, -} +/// The default async callbacks. +impl AsyncCallbacks for () {} /// Describes how new connections to the server are handled. +#[derive(Clone, PartialEq)] #[non_exhaustive] -#[derive(Clone, PartialEq, Default)] pub enum ConnectionMode { /// The "online mode" fetches all player data (username, UUID, and skin) /// from the [configured session server] and enables encryption. /// - /// This mode should be used for all publicly exposed servers which are not + /// This mode should be used by all publicly exposed servers which are not /// behind a proxy. /// - /// [configured session server]: Config::session_server - #[default] - Online, + /// [configured session server]: AsyncCallbacks::session_server + Online { + /// Determines if client IP validation should take place during + /// authentication. + /// + /// When `prevent_proxy_connections` is enabled, clients can no longer + /// log-in if they connected to the Yggdrasil server using a different + /// IP than the one used to connect to this server. + /// + /// This is used by the default implementation of + /// [`AsyncCallbacks::session_server`]. A different implementation may + /// choose to ignore this value. + prevent_proxy_connections: bool, + }, /// Disables client authentication with the configured session server. /// Clients can join with any username and UUID they choose, potentially /// gaining privileges they would not otherwise have. Additionally, @@ -396,32 +387,50 @@ pub enum ConnectionMode { Velocity { /// The secret key used to prevent connections from outside Velocity. /// The proxy and Valence must be configured to use the same secret key. - secret: String, + secret: Arc, }, } -/// A minimal `Config` implementation for testing purposes. -#[cfg(test)] -pub(crate) struct MockConfig { - _marker: std::marker::PhantomData<(S, Cl, E, W, Ch, P, I)>, +/// Minecraft's standard ticks per second (TPS). +pub const DEFAULT_TPS: i64 = 20; + +/// The result of the Server List Ping [callback]. +/// +/// [callback]: crate::config::AsyncCallbacks +#[derive(Clone, Default, Debug)] +pub enum ServerListPing<'a> { + /// Responds to the server list ping with the given information. + Respond { + /// Displayed as the number of players on the server. + online_players: i32, + /// Displayed as the maximum number of players allowed on the server at + /// a time. + max_players: i32, + /// The list of players visible by hovering over the player count. + /// + /// Has no effect if this list is empty. + player_sample: Vec, + /// A description of the server. + description: Text, + /// The server's icon as the bytes of a PNG image. + /// The image must be 64x64 pixels. + /// + /// No icon is used if the slice is empty. + favicon_png: &'a [u8], + }, + /// Ignores the query and disconnects from the client. + #[default] + Ignore, } -#[cfg(test)] -impl Config for MockConfig -where - S: Send + Sync + 'static, - Cl: Default + Send + Sync + 'static, - E: Send + Sync + 'static, - W: Send + Sync + 'static, - Ch: Send + Sync + 'static, - P: Send + Sync + 'static, - I: Send + Sync + 'static, -{ - type ServerState = S; - type ClientState = Cl; - type EntityState = E; - type WorldState = W; - type ChunkState = Ch; - type PlayerListState = P; - type InventoryState = I; +/// Represents an individual entry in the player sample. +#[derive(Clone, Debug, Serialize)] +pub struct PlayerSampleEntry { + /// The name of the player. + /// + /// This string can contain + /// [legacy formatting codes](https://minecraft.fandom.com/wiki/Formatting_codes). + pub name: String, + /// The player UUID. + pub id: Uuid, } diff --git a/crates/valence/src/dimension.rs b/crates/valence/src/dimension.rs index eec7bd5..7f94fa1 100644 --- a/crates/valence/src/dimension.rs +++ b/crates/valence/src/dimension.rs @@ -9,11 +9,13 @@ use crate::LIBRARY_NAMESPACE; /// Identifies a particular [`Dimension`] on the server. /// -/// The default dimension ID refers to the first dimension added in the server's -/// [configuration](crate::config::Config). +/// The default dimension ID refers to the first dimension added in +/// [`ServerPlugin::dimensions`]. /// -/// To obtain dimension IDs for other dimensions, call -/// [`dimensions`](crate::server::SharedServer::dimensions). +/// To obtain dimension IDs for other dimensions, look at +/// [`ServerPlugin::dimensions`]. +/// +/// [`ServerPlugin::dimensions`]: crate::server::SharedServer::dimensions #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct DimensionId(pub(crate) u16); @@ -28,7 +30,9 @@ impl DimensionId { } /// The default dimension ID corresponds to the first element in the `Vec` -/// returned by [`crate::config::Config::dimensions`]. +/// returned by [`ServerPlugin::dimensions`]. +/// +/// [`ServerPlugin::dimensions`]: crate::config::ServerPlugin::dimensions impl Default for DimensionId { fn default() -> Self { Self(0) @@ -37,16 +41,16 @@ impl Default for DimensionId { /// Contains the configuration for a dimension type. /// -/// On creation, each [`World`] in Valence is assigned a dimension. The +/// On creation, each [`Instance`] in Valence is assigned a dimension. The /// dimension determines certain properties of the world such as its height and /// ambient lighting. /// /// In Minecraft, "dimension" and "dimension type" are two distinct concepts. /// For instance, the Overworld and Nether are dimensions, each with /// their own dimension type. A dimension in this library is analogous to a -/// [`World`] while [`Dimension`] represents a dimension type. +/// [`Instance`] while [`Dimension`] represents a dimension type. /// -/// [`World`]: crate::world::World +/// [`Instance`]: crate::instance::Instance #[derive(Clone, Debug)] pub struct Dimension { /// When false, compasses will spin randomly. @@ -70,8 +74,7 @@ pub struct Dimension { /// * `0 <= height <= 4064` /// * `min_y + height <= 2032` pub height: i32, - // TODO: The following fields should be added if they can affect the - // appearance of the dimension to clients. + // TODO: add other fields. // * infiniburn // * monster_spawn_light_level // * monster_spawn_block_light_level diff --git a/crates/valence/src/entity.rs b/crates/valence/src/entity.rs index 133dd42..4517e4d 100644 --- a/crates/valence/src/entity.rs +++ b/crates/valence/src/entity.rs @@ -1,377 +1,242 @@ -//! Entities in a world. - -use std::collections::hash_map::Entry; use std::collections::HashMap; -use std::iter::FusedIterator; -use std::num::NonZeroU32; -use std::ops::{Deref, DerefMut, Index, IndexMut, Range}; +use std::fmt; +use std::fmt::Formatter; +use std::ops::Range; -use bitfield_struct::bitfield; +use bevy_ecs::prelude::*; pub use data::{EntityKind, TrackedData}; -use rayon::iter::ParallelIterator; +use glam::{DVec3, UVec3, Vec3}; +use rustc_hash::FxHashMap; +use tracing::warn; use uuid::Uuid; use valence_protocol::entity_meta::{Facing, PaintingKind, Pose}; use valence_protocol::packets::s2c::play::{ - EntityAnimationS2c, EntityEvent as EntityEventPacket, SetEntityMetadata, SetEntityVelocity, + EntityAnimationS2c, EntityEvent as EntityEventS2c, SetEntityMetadata, SetEntityVelocity, SetHeadRotation, SpawnEntity, SpawnExperienceOrb, SpawnPlayer, TeleportEntity, UpdateEntityPosition, UpdateEntityPositionAndRotation, UpdateEntityRotation, }; use valence_protocol::{ByteAngle, RawBytes, VarInt}; -use vek::{Aabb, Vec3}; -use crate::config::Config; +use crate::config::DEFAULT_TPS; +use crate::math::Aabb; use crate::packet::WritePacket; -use crate::server::PlayPacketSender; -use crate::slab_versioned::{Key, VersionedSlab}; -use crate::util::aabb_from_bottom_and_size; -use crate::world::WorldId; -use crate::STANDARD_TPS; +use crate::{Despawned, NULL_ENTITY}; pub mod data; include!(concat!(env!("OUT_DIR"), "/entity_event.rs")); -/// A container for all [`Entity`]s on a server. -/// -/// # Spawning Player Entities -/// -/// [`Player`] entities are treated specially by the client. For the player -/// entity to be visible to clients, the player's UUID must be added to the -/// [`PlayerList`] _before_ being loaded by the client. -/// -/// [`Player`]: crate::entity::data::Player -/// [`PlayerList`]: crate::player_list::PlayerList -pub struct Entities { - slab: VersionedSlab>, - uuid_to_entity: HashMap, - raw_id_to_entity: HashMap, +/// A [`Resource`] which maintains information about all the [`McEntity`] +/// components on the server. +#[derive(Resource)] +pub struct McEntityManager { + protocol_id_to_entity: FxHashMap, + next_protocol_id: i32, } -impl Entities { +impl McEntityManager { pub(crate) fn new() -> Self { Self { - slab: VersionedSlab::new(), - uuid_to_entity: HashMap::new(), - raw_id_to_entity: HashMap::new(), + protocol_id_to_entity: HashMap::default(), + next_protocol_id: 1, } } - /// Spawns a new entity with a random UUID. A reference to the entity along - /// with its ID is returned. - pub fn insert( - &mut self, - kind: EntityKind, - state: C::EntityState, - ) -> (EntityId, &mut Entity) { - self.insert_with_uuid(kind, Uuid::from_bytes(rand::random()), state) - .expect("UUID collision") + /// Gets the [`Entity`] of the [`McEntity`] with the given protocol ID. + pub fn get_with_protocol_id(&self, id: i32) -> Option { + self.protocol_id_to_entity.get(&id).cloned() } +} - /// Like [`Self::insert`], but requires specifying the new - /// entity's UUID. - /// - /// The provided UUID must not conflict with an existing entity UUID. If it - /// does, `None` is returned and the entity is not spawned. - pub fn insert_with_uuid( - &mut self, - kind: EntityKind, - uuid: Uuid, - state: C::EntityState, - ) -> Option<(EntityId, &mut Entity)> { - match self.uuid_to_entity.entry(uuid) { - Entry::Occupied(_) => None, - Entry::Vacant(ve) => { - let (k, e) = self.slab.insert(Entity { - state, - variants: TrackedData::new(kind), - self_update_range: 0..0, - events: vec![], - bits: EntityBits::new(), - world: WorldId::NULL, - old_world: WorldId::NULL, - position: Vec3::default(), - old_position: Vec3::default(), - yaw: 0.0, - pitch: 0.0, - head_yaw: 0.0, - velocity: Vec3::default(), - uuid, - }); - - // TODO check for overflowing version? - self.raw_id_to_entity.insert(k.version(), k.index()); - - ve.insert(EntityId(k)); - - Some((EntityId(k), e)) - } +/// Sets the protocol ID of new entities. +pub(crate) fn init_entities( + mut entities: Query<(Entity, &mut McEntity), Added>, + mut manager: ResMut, +) { + for (entity, mut mc_entity) in &mut entities { + if manager.next_protocol_id == 0 { + warn!("entity protocol ID overflow"); + // ID 0 is reserved for clients so we skip over it. + manager.next_protocol_id = 1; } - } - /// Returns the number of entities in this container. - pub fn len(&self) -> usize { - self.slab.len() - } + mc_entity.protocol_id = manager.next_protocol_id; + manager.next_protocol_id = manager.next_protocol_id.wrapping_add(1); - /// Returns `true` if there are no entities. - pub fn is_empty(&self) -> bool { - self.slab.len() == 0 - } - - /// Gets the [`EntityId`] of the entity with the given UUID in an efficient - /// manner. The returned ID is guaranteed to be valid. - /// - /// If there is no entity with the UUID, `None` is returned. - pub fn get_with_uuid(&self, uuid: Uuid) -> Option { - self.uuid_to_entity.get(&uuid).cloned() - } - - /// Gets a shared reference to the entity with the given [`EntityId`]. - /// - /// If the ID is invalid, `None` is returned. - pub fn get(&self, entity: EntityId) -> Option<&Entity> { - self.slab.get(entity.0) - } - - /// Gets an exclusive reference to the entity with the given [`EntityId`]. - /// - /// If the ID is invalid, `None` is returned. - pub fn get_mut(&mut self, entity: EntityId) -> Option<&mut Entity> { - self.slab.get_mut(entity.0) - } - - pub fn delete(&mut self, entity: EntityId) -> bool { - if let Some(entity) = self.get_mut(entity) { - entity.set_deleted(true); - true - } else { - false - } - } - - pub fn get_with_raw_id(&self, raw_id: i32) -> Option<(EntityId, &Entity)> { - let version = NonZeroU32::new(raw_id as u32)?; - let index = *self.raw_id_to_entity.get(&version)?; - - let id = EntityId(Key::new(index, version)); - let entity = self.get(id)?; - Some((id, entity)) - } - - pub fn get_with_raw_id_mut(&mut self, raw_id: i32) -> Option<(EntityId, &mut Entity)> { - let version = NonZeroU32::new(raw_id as u32)?; - let index = *self.raw_id_to_entity.get(&version)?; - - let id = EntityId(Key::new(index, version)); - let entity = self.get_mut(id)?; - Some((id, entity)) - } - - /// Returns an iterator over all entities on the server in an unspecified - /// order. - pub fn iter( - &self, - ) -> impl ExactSizeIterator)> + FusedIterator + Clone + '_ { - self.slab.iter().map(|(k, v)| (EntityId(k), v)) - } - - /// Returns a mutable iterator over all entities on the server in an - /// unspecified order. - pub fn iter_mut( - &mut self, - ) -> impl ExactSizeIterator)> + FusedIterator + '_ { - self.slab.iter_mut().map(|(k, v)| (EntityId(k), v)) - } - - /// Returns a parallel iterator over all entities on the server in an - /// unspecified order. - pub fn par_iter(&self) -> impl ParallelIterator)> + Clone + '_ { - self.slab.par_iter().map(|(k, v)| (EntityId(k), v)) - } - - /// Returns a parallel mutable iterator over all clients on the server in an - /// unspecified order. - pub fn par_iter_mut( - &mut self, - ) -> impl ParallelIterator)> + '_ { - self.slab.par_iter_mut().map(|(k, v)| (EntityId(k), v)) - } - - pub(crate) fn update(&mut self) { - self.slab.retain(|k, entity| { - if entity.deleted() { - self.uuid_to_entity - .remove(&entity.uuid) - .expect("UUID should have been in UUID map"); - - self.raw_id_to_entity - .remove(&k.version()) - .expect("raw ID should have been in the raw ID map"); - - false - } else { - entity.old_position = entity.position; - entity.old_world = entity.world; - entity.variants.clear_modifications(); - entity.events.clear(); - - entity.bits.set_yaw_or_pitch_modified(false); - entity.bits.set_head_yaw_modified(false); - entity.bits.set_velocity_modified(false); - - true - } - }); + manager + .protocol_id_to_entity + .insert(mc_entity.protocol_id, entity); } } -impl Index for Entities { - type Output = Entity; - - fn index(&self, index: EntityId) -> &Self::Output { - self.get(index).expect("invalid entity ID") +/// Removes despawned entities from the entity manager. +pub(crate) fn deinit_despawned_entities( + entities: Query<&mut McEntity, With>, + mut manager: ResMut, +) { + for entity in &entities { + manager.protocol_id_to_entity.remove(&entity.protocol_id); } } -impl IndexMut for Entities { - fn index_mut(&mut self, index: EntityId) -> &mut Self::Output { - self.get_mut(index).expect("invalid entity ID") +pub(crate) fn update_entities(mut entities: Query<&mut McEntity, Changed>) { + for mut entity in &mut entities { + entity.data.clear_modifications(); + entity.old_position = entity.position; + entity.old_instance = entity.instance; + entity.statuses = 0; + entity.animations = 0; + entity.yaw_or_pitch_modified = false; + entity.head_yaw_modified = false; + entity.velocity_modified = false; } } -/// An identifier for an [`Entity`] on the server. -/// -/// Entity IDs are either _valid_ or _invalid_. Valid entity IDs point to -/// entities that have not been deleted, while invalid IDs point to those that -/// have. Once an ID becomes invalid, it will never become valid again. -/// -/// The [`Ord`] instance on this type is correct but otherwise unspecified. This -/// is useful for storing IDs in containers such as -/// [`BTreeMap`](std::collections::BTreeMap). -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub struct EntityId(Key); - -impl EntityId { - /// The value of the default entity ID which is always invalid. - pub const NULL: Self = Self(Key::NULL); - - pub fn to_raw(self) -> i32 { - self.0.version().get() as i32 +pub(crate) fn check_entity_invariants(removed: RemovedComponents) { + for entity in &removed { + warn!( + entity = ?entity, + "A `McEntity` component was removed from the world directly. You must use the \ + `Despawned` marker component instead." + ); } } -/// Represents an entity on the server. +/// A component for Minecraft entities. For Valence to recognize a +/// Minecraft entity, it must have this component attached. /// -/// An entity is mostly anything in a world that isn't a block or client. -/// Entities include paintings, falling blocks, zombies, fireballs, and more. +/// ECS entities with this component are not allowed to be removed from the +/// [`World`] directly. Instead, you must mark these entities with [`Despawned`] +/// to allow deinitialization to occur. /// -/// Every entity has common state which is accessible directly from -/// this struct. This includes position, rotation, velocity, UUID, and hitbox. -/// To access data that is not common to every kind of entity, see -/// [`Self::data`]. -pub struct Entity { - /// Custom data. - pub state: C::EntityState, - variants: TrackedData, - bits: EntityBits, +/// Every entity has common state which is accessible directly from this struct. +/// This includes position, rotation, velocity, and UUID. To access data that is +/// not common to every kind of entity, see [`Self::data`]. +#[derive(Component)] +pub struct McEntity { + data: TrackedData, + protocol_id: i32, + uuid: Uuid, /// The range of bytes in the partition cell containing this entity's update /// packets. pub(crate) self_update_range: Range, - events: Vec, // TODO: store this info in bits? - world: WorldId, - old_world: WorldId, - position: Vec3, - old_position: Vec3, + /// Contains a set bit for every status triggered this tick. + statuses: u64, + /// Contains a set bit for every animation triggered this tick. + animations: u8, + instance: Entity, + old_instance: Entity, + position: DVec3, + old_position: DVec3, yaw: f32, pitch: f32, + yaw_or_pitch_modified: bool, head_yaw: f32, - velocity: Vec3, - uuid: Uuid, + head_yaw_modified: bool, + velocity: Vec3, + velocity_modified: bool, + on_ground: bool, } -#[bitfield(u8)] -pub(crate) struct EntityBits { - pub yaw_or_pitch_modified: bool, - pub head_yaw_modified: bool, - pub velocity_modified: bool, - pub on_ground: bool, - pub deleted: bool, - #[bits(3)] - _pad: u8, -} - -impl Deref for Entity { - type Target = C::EntityState; - - fn deref(&self) -> &Self::Target { - &self.state +impl McEntity { + /// Creates a new [`McEntity`] component with a random UUID. + /// + /// - `kind`: The type of Minecraft entity this should be. + /// - `instance`: The [`Entity`] that has an [`Instance`] that this entity + /// will be located in. + /// + /// [`Instance`]: crate::instance::Instance + pub fn new(kind: EntityKind, instance: Entity) -> Self { + Self::with_uuid(kind, instance, Uuid::from_u128(rand::random())) } -} -impl DerefMut for Entity { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.state + /// Like [`Self::new`], but allows specifying the UUID of the entity. + pub fn with_uuid(kind: EntityKind, instance: Entity, uuid: Uuid) -> Self { + Self { + data: TrackedData::new(kind), + self_update_range: 0..0, + statuses: 0, + animations: 0, + instance, + old_instance: NULL_ENTITY, + position: DVec3::ZERO, + old_position: DVec3::ZERO, + yaw: 0.0, + pitch: 0.0, + yaw_or_pitch_modified: false, + head_yaw: 0.0, + head_yaw_modified: false, + velocity: Vec3::ZERO, + velocity_modified: false, + protocol_id: 0, + uuid, + on_ground: false, + } } -} -impl Entity { - /// Returns a shared reference to this entity's tracked data. + /// Returns a reference to this entity's tracked data. pub fn data(&self) -> &TrackedData { - &self.variants + &self.data } - /// Returns an exclusive reference to this entity's tracked data. + /// Returns a mutable reference to this entity's tracked data. pub fn data_mut(&mut self) -> &mut TrackedData { - &mut self.variants + &mut self.data } /// Gets the [`EntityKind`] of this entity. pub fn kind(&self) -> EntityKind { - self.variants.kind() + self.data.kind() } - /// Triggers an entity event for this entity. - pub fn push_event(&mut self, event: EntityEvent) { - self.events.push(event); - } - - /// Gets the [`WorldId`](crate::world::WorldId) of the world this entity is - /// located in. + /// Returns a handle to the [`Instance`] this entity is located in. /// - /// By default, entities are located in - /// [`WorldId::NULL`](crate::world::WorldId::NULL). - pub fn world(&self) -> WorldId { - self.world + /// [`Instance`]: crate::instance::Instance + pub fn instance(&self) -> Entity { + self.instance } - /// Sets the world this entity is located in. - pub fn set_world(&mut self, world: WorldId) { - self.world = world; + /// Sets the [`Instance`] this entity is located in. + /// + /// [`Instance`]: crate::instance::Instance + pub fn set_instance(&mut self, instance: Entity) { + self.instance = instance; } - pub(crate) fn old_world(&self) -> WorldId { - self.old_world + pub(crate) fn old_instance(&self) -> Entity { + self.old_instance + } + + /// Gets the UUID of this entity. + pub fn uuid(&self) -> Uuid { + self.uuid + } + + /// Returns the raw protocol ID of this entity. IDs for new entities are not + /// initialized until the end of the tick. + pub fn protocol_id(&self) -> i32 { + self.protocol_id } /// Gets the position of this entity in the world it inhabits. /// - /// The position of an entity is located on the botton of its + /// The position of an entity is located on the bottom of its /// hitbox and not the center. - pub fn position(&self) -> Vec3 { + pub fn position(&self) -> DVec3 { self.position } /// Sets the position of this entity in the world it inhabits. /// - /// The position of an entity is located on the botton of its + /// The position of an entity is located on the bottom of its /// hitbox and not the center. - pub fn set_position(&mut self, pos: impl Into>) { + pub fn set_position(&mut self, pos: impl Into) { self.position = pos.into(); } /// Returns the position of this entity as it existed at the end of the /// previous tick. - pub(crate) fn old_position(&self) -> Vec3 { + pub(crate) fn old_position(&self) -> DVec3 { self.old_position } @@ -384,7 +249,7 @@ impl Entity { pub fn set_yaw(&mut self, yaw: f32) { if self.yaw != yaw { self.yaw = yaw; - self.bits.set_yaw_or_pitch_modified(true); + self.yaw_or_pitch_modified = true; } } @@ -397,7 +262,7 @@ impl Entity { pub fn set_pitch(&mut self, pitch: f32) { if self.pitch != pitch { self.pitch = pitch; - self.bits.set_yaw_or_pitch_modified(true); + self.yaw_or_pitch_modified = true; } } @@ -410,38 +275,42 @@ impl Entity { pub fn set_head_yaw(&mut self, head_yaw: f32) { if self.head_yaw != head_yaw { self.head_yaw = head_yaw; - self.bits.set_head_yaw_modified(true); + self.head_yaw_modified = true; } } /// Gets the velocity of this entity in meters per second. - pub fn velocity(&self) -> Vec3 { + pub fn velocity(&self) -> Vec3 { self.velocity } /// Sets the velocity of this entity in meters per second. - pub fn set_velocity(&mut self, velocity: impl Into>) { + pub fn set_velocity(&mut self, velocity: impl Into) { let new_vel = velocity.into(); if self.velocity != new_vel { self.velocity = new_vel; - self.bits.set_velocity_modified(true); + self.velocity_modified = true; } } /// Gets the value of the "on ground" flag. pub fn on_ground(&self) -> bool { - self.bits.on_ground() + self.on_ground } /// Sets the value of the "on ground" flag. pub fn set_on_ground(&mut self, on_ground: bool) { - self.bits.set_on_ground(on_ground); + self.on_ground = on_ground; + // TODO: on ground modified flag? } - /// Gets the UUID of this entity. - pub fn uuid(&self) -> Uuid { - self.uuid + pub fn trigger_status(&mut self, status: EntityStatus) { + self.statuses |= 1 << status as u64; + } + + pub fn trigger_animation(&mut self, animation: EntityAnimation) { + self.animations |= 1 << animation as u8; } /// Returns the hitbox of this entity. @@ -452,8 +321,8 @@ impl Entity { /// The hitbox of an entity is determined by its position, entity type, and /// other state specific to that type. /// - /// [interact event]: crate::client::ClientEvent::InteractWithEntity - pub fn hitbox(&self) -> Aabb { + /// [interact event]: crate::client::event::InteractWithEntity + pub fn hitbox(&self) -> Aabb { fn baby(is_baby: bool, adult_hitbox: [f64; 3]) -> [f64; 3] { if is_baby { adult_hitbox.map(|a| a / 2.0) @@ -462,7 +331,7 @@ impl Entity { } } - fn item_frame(pos: Vec3, rotation: i32) -> Aabb { + fn item_frame(pos: DVec3, rotation: i32) -> Aabb { let mut center_pos = pos + 0.5; match rotation { @@ -475,7 +344,7 @@ impl Entity { _ => center_pos.y -= 0.46875, }; - let bounds = Vec3::from(match rotation { + let bounds = DVec3::from(match rotation { 0 | 1 => [0.75, 0.0625, 0.75], 2 | 3 => [0.75, 0.75, 0.0625], 4 | 5 => [0.0625, 0.75, 0.75], @@ -488,7 +357,7 @@ impl Entity { } } - let dimensions = match &self.variants { + let dimensions = match &self.data { TrackedData::Allay(_) => [0.6, 0.35, 0.6], TrackedData::ChestBoat(_) => [1.375, 0.5625, 1.375], TrackedData::Frog(_) => [0.5, 0.5, 0.5], @@ -580,7 +449,7 @@ impl Entity { TrackedData::Mooshroom(e) => baby(e.get_child(), [0.9, 1.4, 0.9]), TrackedData::Ocelot(e) => baby(e.get_child(), [0.6, 0.7, 0.6]), TrackedData::Painting(e) => { - let bounds: Vec3 = match e.get_variant() { + let bounds: UVec3 = match e.get_variant() { PaintingKind::Kebab => [1, 1, 1], PaintingKind::Aztec => [1, 1, 1], PaintingKind::Alban => [1, 1, 1], @@ -632,8 +501,8 @@ impl Entity { center_pos.z += cc_facing_z as f64 * if bounds.z % 2 == 0 { 0.5 } else { 0.0 }; let bounds = match (facing_x, facing_z) { - (1, 0) | (-1, 0) => bounds.as_().with_x(0.0625), - _ => bounds.as_().with_z(0.0625), + (1, 0) | (-1, 0) => DVec3::new(0.0625, bounds.y as f64, bounds.z as f64), + _ => DVec3::new(bounds.x as f64, bounds.y as f64, 0.0625), }; return Aabb { @@ -732,269 +601,193 @@ impl Entity { TrackedData::FishingBobber(_) => [0.25, 0.25, 0.25], }; - aabb_from_bottom_and_size(self.position, dimensions.into()) - } - - pub fn deleted(&self) -> bool { - self.bits.deleted() - } - - pub fn set_deleted(&mut self, deleted: bool) { - self.bits.set_deleted(deleted) + Aabb::from_bottom_size(self.position, dimensions) } /// Sends the appropriate packets to initialize the entity. This will spawn /// the entity and initialize tracked data. - pub(crate) fn send_init_packets( + pub(crate) fn write_init_packets( &self, - send: &mut PlayPacketSender, - position: Vec3, - this_id: EntityId, + mut writer: impl WritePacket, + position: DVec3, scratch: &mut Vec, - ) -> anyhow::Result<()> { + ) { let with_object_data = |data| SpawnEntity { - entity_id: VarInt(this_id.to_raw()), + entity_id: VarInt(self.protocol_id), object_uuid: self.uuid, kind: VarInt(self.kind() as i32), - position: position.into_array(), + position: position.to_array(), pitch: ByteAngle::from_degrees(self.pitch), yaw: ByteAngle::from_degrees(self.yaw), head_yaw: ByteAngle::from_degrees(self.head_yaw), data: VarInt(data), - velocity: velocity_to_packet_units(self.velocity).into_array(), + velocity: velocity_to_packet_units(self.velocity), }; - match &self.variants { + match &self.data { TrackedData::Marker(_) => {} - TrackedData::ExperienceOrb(_) => send.append_packet(&SpawnExperienceOrb { - entity_id: VarInt(this_id.to_raw()), - position: position.into_array(), + TrackedData::ExperienceOrb(_) => writer.write_packet(&SpawnExperienceOrb { + entity_id: VarInt(self.protocol_id), + position: position.to_array(), count: 0, // TODO - })?, + }), TrackedData::Player(_) => { - send.append_packet(&SpawnPlayer { - entity_id: VarInt(this_id.to_raw()), + writer.write_packet(&SpawnPlayer { + entity_id: VarInt(self.protocol_id), player_uuid: self.uuid, - position: position.into_array(), + position: position.to_array(), yaw: ByteAngle::from_degrees(self.yaw), pitch: ByteAngle::from_degrees(self.pitch), - })?; + }); // Player spawn packet doesn't include head yaw for some reason. - send.append_packet(&SetHeadRotation { - entity_id: VarInt(this_id.to_raw()), + writer.write_packet(&SetHeadRotation { + entity_id: VarInt(self.protocol_id), head_yaw: ByteAngle::from_degrees(self.head_yaw), - })?; + }); } - TrackedData::ItemFrame(e) => send.append_packet(&with_object_data(e.get_rotation()))?, + TrackedData::ItemFrame(e) => writer.write_packet(&with_object_data(e.get_rotation())), TrackedData::GlowItemFrame(e) => { - send.append_packet(&with_object_data(e.get_rotation()))? + writer.write_packet(&with_object_data(e.get_rotation())) } - TrackedData::Painting(_) => send.append_packet(&with_object_data( + TrackedData::Painting(_) => writer.write_packet(&with_object_data( match ((self.yaw + 45.0).rem_euclid(360.0) / 90.0) as u8 { 0 => 3, 1 => 4, 2 => 2, _ => 5, }, - ))?, + )), // TODO: set block state ID for falling block. - TrackedData::FallingBlock(_) => send.append_packet(&with_object_data(1))?, + TrackedData::FallingBlock(_) => writer.write_packet(&with_object_data(1)), TrackedData::FishingBobber(e) => { - send.append_packet(&with_object_data(e.get_hook_entity_id()))? + writer.write_packet(&with_object_data(e.get_hook_entity_id())) } TrackedData::Warden(e) => { - send.append_packet(&with_object_data((e.get_pose() == Pose::Emerging).into()))? + writer.write_packet(&with_object_data((e.get_pose() == Pose::Emerging).into())) } - _ => send.append_packet(&with_object_data(0))?, + _ => writer.write_packet(&with_object_data(0)), } scratch.clear(); - self.variants.write_initial_tracked_data(scratch); + self.data.write_initial_tracked_data(scratch); if !scratch.is_empty() { - send.append_packet(&SetEntityMetadata { - entity_id: VarInt(this_id.to_raw()), + writer.write_packet(&SetEntityMetadata { + entity_id: VarInt(self.protocol_id), metadata: RawBytes(scratch), - })?; + }); } - - Ok(()) } /// Writes the appropriate packets to update the entity (Position, tracked - /// data, and event packets). - pub(crate) fn write_update_packets( - &self, - mut writer: impl WritePacket, - this_id: EntityId, - scratch: &mut Vec, - ) -> anyhow::Result<()> { - let entity_id = VarInt(this_id.to_raw()); + /// data, events, animations). + pub(crate) fn write_update_packets(&self, mut writer: impl WritePacket, scratch: &mut Vec) { + let entity_id = VarInt(self.protocol_id); let position_delta = self.position - self.old_position; - let needs_teleport = position_delta.map(f64::abs).reduce_partial_max() >= 8.0; + let needs_teleport = position_delta.abs().max_element() >= 8.0; let changed_position = self.position != self.old_position; - if changed_position && !needs_teleport && self.bits.yaw_or_pitch_modified() { + if changed_position && !needs_teleport && self.yaw_or_pitch_modified { writer.write_packet(&UpdateEntityPositionAndRotation { entity_id, - delta: (position_delta * 4096.0).as_::().into_array(), + delta: (position_delta * 4096.0).to_array().map(|v| v as i16), yaw: ByteAngle::from_degrees(self.yaw), pitch: ByteAngle::from_degrees(self.pitch), - on_ground: self.bits.on_ground(), - })?; + on_ground: self.on_ground, + }); } else { if changed_position && !needs_teleport { writer.write_packet(&UpdateEntityPosition { entity_id, - delta: (position_delta * 4096.0).as_::().into_array(), - on_ground: self.bits.on_ground(), - })?; + delta: (position_delta * 4096.0).to_array().map(|v| v as i16), + on_ground: self.on_ground, + }); } - if self.bits.yaw_or_pitch_modified() { + if self.yaw_or_pitch_modified { writer.write_packet(&UpdateEntityRotation { entity_id, yaw: ByteAngle::from_degrees(self.yaw), pitch: ByteAngle::from_degrees(self.pitch), - on_ground: self.bits.on_ground(), - })?; + on_ground: self.on_ground, + }); } } if needs_teleport { writer.write_packet(&TeleportEntity { entity_id, - position: self.position.into_array(), + position: self.position.to_array(), yaw: ByteAngle::from_degrees(self.yaw), pitch: ByteAngle::from_degrees(self.pitch), - on_ground: self.bits.on_ground(), - })?; + on_ground: self.on_ground, + }); } - if self.bits.velocity_modified() { + if self.velocity_modified { writer.write_packet(&SetEntityVelocity { entity_id, - velocity: velocity_to_packet_units(self.velocity).into_array(), - })?; + velocity: velocity_to_packet_units(self.velocity), + }); } - if self.bits.head_yaw_modified() { + if self.head_yaw_modified { writer.write_packet(&SetHeadRotation { entity_id, head_yaw: ByteAngle::from_degrees(self.head_yaw), - })?; + }); } scratch.clear(); - self.variants.write_updated_tracked_data(scratch); + self.data.write_updated_tracked_data(scratch); if !scratch.is_empty() { writer.write_packet(&SetEntityMetadata { entity_id, metadata: RawBytes(scratch), - })?; + }); } - for &event in &self.events { - match event.status_or_animation() { - StatusOrAnimation::Status(code) => writer.write_packet(&EntityEventPacket { - entity_id: entity_id.0, - entity_status: code, - })?, - StatusOrAnimation::Animation(code) => writer.write_packet(&EntityAnimationS2c { - entity_id, - animation: code, - })?, + if self.statuses != 0 { + for i in 0..std::mem::size_of_val(&self.statuses) { + if (self.statuses >> i) & 1 == 1 { + writer.write_packet(&EntityEventS2c { + entity_id: entity_id.0, + entity_status: i as u8, + }); + } } } - Ok(()) + if self.animations != 0 { + for i in 0..std::mem::size_of_val(&self.animations) { + if (self.animations >> i) & 1 == 1 { + writer.write_packet(&EntityAnimationS2c { + entity_id, + animation: i as u8, + }); + } + } + } } } -pub(crate) fn velocity_to_packet_units(vel: Vec3) -> Vec3 { - // The saturating cast to i16 is desirable. - (8000.0 / STANDARD_TPS as f32 * vel).as_() +#[inline] +pub(crate) fn velocity_to_packet_units(vel: Vec3) -> [i16; 3] { + // The saturating casts to i16 are desirable. + (8000.0 / DEFAULT_TPS as f32 * vel) + .to_array() + .map(|v| v as i16) } -#[cfg(test)] -mod tests { - use super::*; - - type MockConfig = crate::config::MockConfig<(), (), u8>; - - #[test] - fn entities_has_valid_new_state() { - let mut entities: Entities = Entities::new(); - let raw_id: i32 = 8675309; - let entity_id = EntityId(Key::new( - 202298, - NonZeroU32::new(raw_id as u32).expect("value given should never be zero!"), - )); - let uuid = Uuid::from_bytes([2; 16]); - assert!(entities.is_empty()); - assert!(entities.get(entity_id).is_none()); - assert!(entities.get_mut(entity_id).is_none()); - assert!(entities.get_with_uuid(uuid).is_none()); - assert!(entities.get_with_raw_id(raw_id).is_none()); - } - - #[test] - fn entities_can_be_set_and_get() { - let mut entities: Entities = Entities::new(); - assert!(entities.is_empty()); - let (player_id, player_entity) = entities.insert(EntityKind::Player, 1); - assert_eq!(player_entity.state, 1); - assert_eq!(entities.get(player_id).unwrap().state, 1); - let mut_player_entity = entities - .get_mut(player_id) - .expect("failed to get mutable reference"); - mut_player_entity.state = 100; - assert_eq!(entities.get(player_id).unwrap().state, 100); - assert_eq!(entities.len(), 1); - } - - #[test] - fn entities_can_be_set_and_get_with_uuid() { - let mut entities: Entities = Entities::new(); - let uuid = Uuid::from_bytes([2; 16]); - assert!(entities.is_empty()); - let (zombie_id, zombie_entity) = entities - .insert_with_uuid(EntityKind::Zombie, uuid, 1) - .expect("unexpected Uuid collision when inserting to an empty collection"); - assert_eq!(zombie_entity.state, 1); - let maybe_zombie = entities - .get_with_uuid(uuid) - .expect("UUID lookup failed on item already added to this collection"); - assert_eq!(zombie_id, maybe_zombie); - assert_eq!(entities.len(), 1); - } - - #[test] - fn entities_can_be_set_and_get_with_raw_id() { - let mut entities: Entities = Entities::new(); - assert!(entities.is_empty()); - let (boat_id, boat_entity) = entities.insert(EntityKind::Boat, 12); - assert_eq!(boat_entity.state, 12); - let (cat_id, cat_entity) = entities.insert(EntityKind::Cat, 75); - assert_eq!(cat_entity.state, 75); - let maybe_boat_id = entities - .get_with_raw_id(boat_id.0.version.get() as i32) - .expect("raw id lookup failed on item already added to this collection") - .0; - let maybe_boat = entities - .get(maybe_boat_id) - .expect("failed to look up item already added to collection"); - assert_eq!(maybe_boat.state, 12); - let maybe_cat_id = entities - .get_with_raw_id(cat_id.0.version.get() as i32) - .expect("raw id lookup failed on item already added to this collection") - .0; - let maybe_cat = entities - .get(maybe_cat_id) - .expect("failed to look up item already added to collection"); - assert_eq!(maybe_cat.state, 75); - assert_eq!(entities.len(), 2); +impl fmt::Debug for McEntity { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct("McEntity") + .field("kind", &self.kind()) + .field("protocol_id", &self.protocol_id) + .field("uuid", &self.uuid) + .field("position", &self.position) + .finish_non_exhaustive() } } diff --git a/crates/valence/src/entity/data.rs b/crates/valence/src/entity/data.rs index cef3dfb..36272e4 100644 --- a/crates/valence/src/entity/data.rs +++ b/crates/valence/src/entity/data.rs @@ -1,6 +1,7 @@ -//! Contains the [`TrackedData`] and the types for each variant. +//! Contains the [`TrackedData`] enum and the types for each variant. -#![allow(clippy::all, missing_docs, trivial_numeric_casts)] +// TODO: clean this up. +#![allow(clippy::all, missing_docs, trivial_numeric_casts, dead_code)] use uuid::Uuid; use valence_protocol::entity_meta::*; diff --git a/crates/valence/src/instance.rs b/crates/valence/src/instance.rs new file mode 100644 index 0000000..c42b3f0 --- /dev/null +++ b/crates/valence/src/instance.rs @@ -0,0 +1,550 @@ +use std::collections::hash_map::Entry; +use std::collections::BTreeSet; +use std::iter::FusedIterator; + +use bevy_ecs::prelude::*; +pub use chunk_entry::*; +use glam::{DVec3, Vec3}; +use num::integer::div_ceil; +use rustc_hash::FxHashMap; +use valence_protocol::block::BlockState; +use valence_protocol::packets::s2c::particle::{Particle, ParticleS2c}; +use valence_protocol::packets::s2c::play::SetActionBarText; +use valence_protocol::{BlockPos, EncodePacket, LengthPrefixedArray, Text}; + +use crate::dimension::DimensionId; +use crate::entity::McEntity; +pub use crate::instance::chunk::Chunk; +use crate::packet::{PacketWriter, WritePacket}; +use crate::server::{Server, SharedServer}; +use crate::view::ChunkPos; +use crate::Despawned; + +mod chunk; +mod chunk_entry; +mod paletted_container; + +/// An Instance represents a Minecraft world, which consist of [`Chunk`]s. +/// It manages updating clients when chunks change, and caches chunk and entity +/// update packets on a per-chunk basis. +/// +/// To create a new instance, use [`SharedServer::new_instance`]. +/// ``` +/// use bevy_app::prelude::*; +/// use valence::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugin(ServerPlugin::new(())); +/// let server = app.world.get_resource::().unwrap(); +/// let instance = server.new_instance(DimensionId::default()); +/// ``` +/// Now you can actually spawn a new [`Entity`] with `instance`. +/// ``` +/// # use bevy_app::prelude::*; +/// # use valence::prelude::*; +/// # let mut app = App::new(); +/// # app.add_plugin(ServerPlugin::new(())); +/// # let server = app.world.get_resource::().unwrap(); +/// # let instance = server.new_instance(DimensionId::default()); +/// let instance_entity = app.world.spawn(instance); +/// ``` +#[derive(Component)] +pub struct Instance { + pub(crate) partition: FxHashMap, + pub(crate) info: InstanceInfo, + /// Packet data to send to all clients in this instance at the end of the + /// tick. + pub(crate) packet_buf: Vec, + /// Scratch space for writing packets. + scratch: Vec, +} + +pub(crate) struct InstanceInfo { + dimension: DimensionId, + section_count: usize, + min_y: i32, + biome_registry_len: usize, + compression_threshold: Option, + filler_sky_light_mask: Box<[u64]>, + /// Sending filler light data causes the vanilla client to lag + /// less. Hopefully we can remove this in the future. + filler_sky_light_arrays: Box<[LengthPrefixedArray]>, +} + +#[derive(Debug)] +pub(crate) struct PartitionCell { + /// The chunk in this cell. + pub(crate) chunk: Option>, + /// If `chunk` went from `Some` to `None` this tick. + pub(crate) chunk_removed: bool, + /// Minecraft entities in this cell. + pub(crate) entities: BTreeSet, + /// Minecraft entities that have entered the chunk this tick, paired with + /// the cell position in this instance they came from. + pub(crate) incoming: Vec<(Entity, Option)>, + /// Minecraft entities that have left the chunk this tick, paired with the + /// cell position in this world they arrived at. + pub(crate) outgoing: Vec<(Entity, Option)>, + /// A cache of packets to send to all clients that are in view of this cell + /// at the end of the tick. + pub(crate) packet_buf: Vec, +} + +impl Instance { + pub(crate) fn new(dimension: DimensionId, shared: &SharedServer) -> Self { + let dim = shared.dimension(dimension); + + let light_section_count = (dim.height / 16 + 2) as usize; + + let mut sky_light_mask = vec![0; div_ceil(light_section_count, 16)]; + + for i in 0..light_section_count { + sky_light_mask[i / 64] |= 1 << (i % 64); + } + + Self { + partition: FxHashMap::default(), + info: InstanceInfo { + dimension, + section_count: (dim.height / 16) as usize, + min_y: dim.min_y, + biome_registry_len: shared.biomes().len(), + compression_threshold: shared.compression_threshold(), + filler_sky_light_mask: sky_light_mask.into(), + filler_sky_light_arrays: vec![ + LengthPrefixedArray([0xff; 2048]); + light_section_count + ] + .into(), + }, + packet_buf: vec![], + scratch: vec![], + } + } + + pub fn dimension(&self) -> DimensionId { + self.info.dimension + } + + pub fn section_count(&self) -> usize { + self.info.section_count + } + + /// Get a reference to the chunk at the given position, if it is loaded. + pub fn chunk(&self, pos: impl Into) -> Option<&Chunk> { + self.partition + .get(&pos.into()) + .and_then(|p| p.chunk.as_ref()) + } + + /// Get a mutable reference to the chunk at the given position, if it is + /// loaded. + pub fn chunk_mut(&mut self, pos: impl Into) -> Option<&mut Chunk> { + self.partition + .get_mut(&pos.into()) + .and_then(|p| p.chunk.as_mut()) + } + + /// Insert a chunk into the instance at the given position. This effectively + /// loads the Chunk. + pub fn insert_chunk(&mut self, pos: impl Into, chunk: Chunk) -> Option { + match self.chunk_entry(pos) { + ChunkEntry::Occupied(mut oe) => Some(oe.insert(chunk)), + ChunkEntry::Vacant(ve) => { + ve.insert(chunk); + None + } + } + } + + /// Unload the chunk at the given position, if it is loaded. Returns the + /// chunk if it was loaded. + pub fn remove_chunk(&mut self, pos: impl Into) -> Option { + match self.chunk_entry(pos) { + ChunkEntry::Occupied(oe) => Some(oe.remove()), + ChunkEntry::Vacant(_) => None, + } + } + + /// Unload all chunks in this instance. + pub fn clear_chunks(&mut self) { + self.retain_chunks(|_, _| false) + } + + /// Retain only the chunks for which the given predicate returns `true`. + pub fn retain_chunks(&mut self, mut f: F) + where + F: FnMut(ChunkPos, &mut Chunk) -> bool, + { + for (&pos, cell) in &mut self.partition { + if let Some(chunk) = &mut cell.chunk { + if !f(pos, chunk) { + cell.chunk = None; + cell.chunk_removed = true; + } + } + } + } + + /// Get a [`ChunkEntry`] for the given position. + pub fn chunk_entry(&mut self, pos: impl Into) -> ChunkEntry { + ChunkEntry::new(self.info.section_count, self.partition.entry(pos.into())) + } + + /// Get an iterator over all loaded chunks in the instance. The order of the + /// chunks is undefined. + pub fn chunks(&self) -> impl FusedIterator)> + Clone + '_ { + self.partition + .iter() + .flat_map(|(&pos, par)| par.chunk.as_ref().map(|c| (pos, c))) + } + + /// Get an iterator over all loaded chunks in the instance, mutably. The + /// order of the chunks is undefined. + pub fn chunks_mut(&mut self) -> impl FusedIterator)> + '_ { + self.partition + .iter_mut() + .flat_map(|(&pos, par)| par.chunk.as_mut().map(|c| (pos, c))) + } + + /// Optimizes the memory usage of the instance. + pub fn optimize(&mut self) { + for (_, chunk) in self.chunks_mut() { + chunk.optimize(); + } + + self.partition.shrink_to_fit(); + self.packet_buf.shrink_to_fit(); + } + + /// Gets the block state at an absolute block position in world space. Only + /// works for blocks in loaded chunks. + /// + /// If the position is not inside of a chunk, then [`BlockState::AIR`] is + /// returned. + pub fn block_state(&self, pos: impl Into) -> BlockState { + let pos = pos.into(); + + let Some(y) = pos.y.checked_sub(self.info.min_y).and_then(|y| y.try_into().ok()) else { + return BlockState::AIR; + }; + + if y >= self.info.section_count * 16 { + return BlockState::AIR; + } + + let Some(chunk) = self.chunk(ChunkPos::from_block_pos(pos)) else { + return BlockState::AIR; + }; + + chunk.block_state( + pos.x.rem_euclid(16) as usize, + y, + pos.z.rem_euclid(16) as usize, + ) + } + + /// Sets the block state at an absolute block position in world space. The + /// previous block state at the position is returned. + /// + /// If the position is not within a loaded chunk or otherwise out of bounds, + /// then [`BlockState::AIR`] is returned with no effect. + pub fn set_block_state(&mut self, pos: impl Into, block: BlockState) -> BlockState { + let pos = pos.into(); + + let Some(y) = pos.y.checked_sub(self.info.min_y).and_then(|y| y.try_into().ok()) else { + return BlockState::AIR; + }; + + if y >= self.info.section_count * 16 { + return BlockState::AIR; + } + + let Some(chunk) = self.chunk_mut(ChunkPos::from_block_pos(pos)) else { + return BlockState::AIR; + }; + + chunk.set_block_state( + pos.x.rem_euclid(16) as usize, + y, + pos.z.rem_euclid(16) as usize, + block, + ) + } + + /// Writes a packet into the global packet buffer of this instance. All + /// clients in the instance will receive the packet. + /// + /// This is more efficient than sending the packet to each client + /// individually. + pub fn write_packet

(&mut self, pkt: &P) + where + P: EncodePacket + ?Sized, + { + PacketWriter::new( + &mut self.packet_buf, + self.info.compression_threshold, + &mut self.scratch, + ) + .write_packet(pkt); + } + + /// Writes arbitrary packet data into the global packet buffer of this + /// instance. All clients in the instance will receive the packet data. + /// + /// The packet data must be properly compressed for the current compression + /// threshold but never encrypted. Don't use this function unless you know + /// what you're doing. Consider using [`Self::write_packet`] instead. + pub fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.packet_buf.extend_from_slice(bytes) + } + + /// Writes a packet to all clients in view of `pos` in this instance. Has no + /// effect if there is no chunk at `pos`. + /// + /// This is more efficient than sending the packet to each client + /// individually. + pub fn write_packet_at

(&mut self, pkt: &P, pos: impl Into) + where + P: EncodePacket + ?Sized, + { + let pos = pos.into(); + if let Some(cell) = self.partition.get_mut(&pos) { + if cell.chunk.is_some() { + PacketWriter::new( + &mut cell.packet_buf, + self.info.compression_threshold, + &mut self.scratch, + ) + .write_packet(pkt); + } + } + } + + /// Writes arbitrary packet data to all clients in view of `pos` in this + /// instance. Has no effect if there is no chunk at `pos`. + /// + /// The packet data must be properly compressed for the current compression + /// threshold but never encrypted. Don't use this function unless you know + /// what you're doing. Consider using [`Self::write_packet`] instead. + pub fn write_packet_bytes_at(&mut self, bytes: &[u8], pos: impl Into) { + let pos = pos.into(); + if let Some(cell) = self.partition.get_mut(&pos) { + if cell.chunk.is_some() { + cell.packet_buf.extend_from_slice(bytes); + } + } + } + + /// Puts a particle effect at the given position in the world. The particle + /// effect is visible to all players in the instance with the + /// appropriate chunk in view. + pub fn play_particle( + &mut self, + particle: &Particle, + long_distance: bool, + position: impl Into, + offset: impl Into, + max_speed: f32, + count: i32, + ) { + let position = position.into(); + + self.write_packet_at( + &ParticleS2c { + particle: particle.clone(), + long_distance, + position: position.into(), + offset: offset.into().into(), + max_speed, + count, + }, + ChunkPos::from_dvec3(position), + ); + } + + /// Sets the action bar text of all players in the instance. + pub fn set_action_bar(&mut self, text: impl Into) { + self.write_packet(&SetActionBarText { + action_bar_text: text.into().into(), + }); + } +} + +pub(crate) fn update_instances_pre_client( + mut instances: Query<&mut Instance>, + mut entities: Query<(Entity, &mut McEntity, Option<&Despawned>)>, + server: Res, +) { + for (entity_id, entity, despawned) in &entities { + let pos = ChunkPos::at(entity.position().x, entity.position().z); + let old_pos = ChunkPos::at(entity.old_position().x, entity.old_position().z); + + let instance = entity.instance(); + let old_instance = entity.old_instance(); + + if despawned.is_some() { + // Entity was deleted. Remove it from the chunk it was in, if it was in a chunk + // at all. + if let Ok(mut old_instance) = instances.get_mut(old_instance) { + if let Some(old_cell) = old_instance.partition.get_mut(&old_pos) { + if old_cell.entities.remove(&entity_id) { + old_cell.outgoing.push((entity_id, None)); + } + } + } + } else if old_instance != instance { + // Entity changed the instance it is in. Remove it from old cell and + // insert it in the new cell. + + // TODO: skip marker entity? + + if let Ok(mut old_instance) = instances.get_mut(old_instance) { + if let Some(old_cell) = old_instance.partition.get_mut(&old_pos) { + if old_cell.entities.remove(&entity_id) { + old_cell.outgoing.push((entity_id, None)); + } + } + } + + if let Ok(mut instance) = instances.get_mut(instance) { + match instance.partition.entry(pos) { + Entry::Occupied(oe) => { + let cell = oe.into_mut(); + if cell.entities.insert(entity_id) { + cell.incoming.push((entity_id, None)); + } + } + Entry::Vacant(ve) => { + ve.insert(PartitionCell { + chunk: None, + chunk_removed: false, + entities: BTreeSet::from([entity_id]), + incoming: vec![(entity_id, None)], + outgoing: vec![], + packet_buf: vec![], + }); + } + } + } + } else if pos != old_pos { + // Entity changed its chunk position without changing instances. Remove + // it from old cell and insert it in new cell. + + // TODO: skip marker entity? + + if let Ok(mut instance) = instances.get_mut(instance) { + if let Some(old_cell) = instance.partition.get_mut(&old_pos) { + if old_cell.entities.remove(&entity_id) { + old_cell.outgoing.push((entity_id, Some(pos))); + } + } + + match instance.partition.entry(pos) { + Entry::Occupied(oe) => { + let cell = oe.into_mut(); + if cell.entities.insert(entity_id) { + cell.incoming.push((entity_id, Some(old_pos))); + } + } + Entry::Vacant(ve) => { + ve.insert(PartitionCell { + chunk: None, + chunk_removed: false, + entities: BTreeSet::from([entity_id]), + incoming: vec![(entity_id, Some(old_pos))], + outgoing: vec![], + packet_buf: vec![], + }); + } + } + } + } else { + // The entity didn't change its chunk position so there is nothing + // we need to do. + } + } + + let mut scratch_1 = vec![]; + let mut scratch_2 = vec![]; + + for instance in &mut instances { + let instance = instance.into_inner(); + + for (&pos, cell) in &mut instance.partition { + // Cache chunk update packets into the packet buffer of this cell. + if let Some(chunk) = &mut cell.chunk { + let writer = PacketWriter::new( + &mut cell.packet_buf, + server.compression_threshold(), + &mut scratch_2, + ); + + chunk.write_update_packets(writer, &mut scratch_1, pos, &instance.info); + + chunk.clear_viewed(); + } + + // Cache entity update packets into the packet buffer of this cell. + for &id in &cell.entities { + let (_, mut entity, despawned) = entities + .get_mut(id) + .expect("missing entity in partition cell"); + + if despawned.is_some() { + continue; + } + + let start = cell.packet_buf.len(); + + let writer = PacketWriter::new( + &mut cell.packet_buf, + server.compression_threshold(), + &mut scratch_2, + ); + + entity.write_update_packets(writer, &mut scratch_1); + + let end = cell.packet_buf.len(); + + entity.self_update_range = start..end; + } + } + } +} + +pub(crate) fn update_instances_post_client(mut instances: Query<&mut Instance>) { + for mut instance in &mut instances { + instance.partition.retain(|_, cell| { + cell.packet_buf.clear(); + cell.chunk_removed = false; + cell.incoming.clear(); + cell.outgoing.clear(); + + if let Some(chunk) = &mut cell.chunk { + chunk.update_post_client(); + } + + cell.chunk.is_some() || !cell.entities.is_empty() + }); + + instance.packet_buf.clear(); + } +} + +pub(crate) fn check_instance_invariants(instances: Query<&Instance>, entities: Query<&McEntity>) { + #[cfg(debug_assertions)] + for instance in &instances { + for (pos, cell) in &instance.partition { + for &id in &cell.entities { + assert!( + entities.get(id).is_ok(), + "instance contains an entity that does not exist at {pos:?}" + ); + } + } + } + + let _ = instances; + let _ = entities; +} diff --git a/crates/valence/src/instance/chunk.rs b/crates/valence/src/instance/chunk.rs new file mode 100644 index 0000000..7d74215 --- /dev/null +++ b/crates/valence/src/instance/chunk.rs @@ -0,0 +1,568 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +// Using nonstandard mutex to avoid poisoning API. +use parking_lot::Mutex; +use valence_nbt::compound; +use valence_protocol::block::BlockState; +use valence_protocol::packets::s2c::play::{ + BlockUpdate, ChunkDataAndUpdateLightEncode, UpdateSectionBlocksEncode, +}; +use valence_protocol::{BlockPos, Encode, VarInt, VarLong}; + +use crate::biome::BiomeId; +use crate::instance::paletted_container::PalettedContainer; +use crate::instance::InstanceInfo; +use crate::math::bit_width; +use crate::packet::{PacketWriter, WritePacket}; +use crate::view::ChunkPos; + +/// A chunk is a 16x16-meter segment of a world with a variable height. Chunks +/// primarily contain blocks, biomes, and block entities. +/// +/// All chunks in an instance have the same height. +#[derive(Debug)] +pub struct Chunk { + sections: Vec

, + /// Cached bytes of the chunk data packet. The cache is considered + /// invalidated if empty. + cached_init_packets: Mutex>, + /// If clients should receive the chunk data packet instead of block change + /// packets on update. + refresh: bool, + /// Tracks if any clients are in view of this (loaded) chunk. Useful for + /// knowing when a chunk should be unloaded. + viewed: AtomicBool, +} + +#[derive(Clone, Default, Debug)] +struct Section { + block_states: PalettedContainer, + biomes: PalettedContainer, + /// Number of non-air blocks in this section. This invariant is maintained + /// even if `track_changes` is false. + non_air_count: u16, + /// Contains modifications for the update section packet. (Or the regular + /// block update packet if len == 1). + section_updates: Vec, +} + +const SECTION_BLOCK_COUNT: usize = 16 * 16 * 16; +const SECTION_BIOME_COUNT: usize = 4 * 4 * 4; + +impl Chunk { + /// Constructs a new chunk containing only [`BlockState::AIR`] and + /// [`BiomeId::default()`] with the given number of sections. A section is a + /// 16x16x16 meter volume. + pub fn new(section_count: usize) -> Self { + let mut chunk = Self { + sections: vec![], + cached_init_packets: Mutex::new(vec![]), + refresh: true, + viewed: AtomicBool::new(false), + }; + + chunk.resize(section_count); + chunk + } + + /// Changes the section count of the chunk to `new_section_count`. + /// + /// The chunk is extended and truncated from the top. New blocks are always + /// [`BlockState::AIR`] and biomes are [`BiomeId::default()`]. + pub fn resize(&mut self, new_section_count: usize) { + let old_section_count = self.section_count(); + + if new_section_count > old_section_count { + self.sections + .reserve_exact(new_section_count - old_section_count); + self.sections + .resize_with(new_section_count, Section::default); + } else { + self.sections.truncate(new_section_count); + } + } + + pub(super) fn into_loaded(self) -> Chunk { + debug_assert!(self.refresh); + + Chunk { + sections: self.sections, + cached_init_packets: self.cached_init_packets, + refresh: true, + viewed: AtomicBool::new(false), + } + } +} + +impl Default for Chunk { + fn default() -> Self { + Self::new(0) + } +} + +impl Clone for Chunk { + fn clone(&self) -> Self { + Self { + sections: self.sections.clone(), + cached_init_packets: Mutex::new(vec![]), + refresh: true, + viewed: AtomicBool::new(false), + } + } +} + +impl Chunk { + /// Creates an unloaded clone of this loaded chunk. + pub fn to_unloaded(&self) -> Chunk { + let sections = self + .sections + .iter() + .map(|sect| { + Section { + block_states: sect.block_states.clone(), + biomes: sect.biomes.clone(), + non_air_count: 0, + section_updates: vec![], // Don't clone the section updates. + } + }) + .collect(); + + Chunk { + sections, + cached_init_packets: Mutex::new(vec![]), + refresh: true, + viewed: AtomicBool::new(false), + } + } + + pub(super) fn clear_viewed(&mut self) { + *self.viewed.get_mut() = false; + } + + /// Returns `true` if this chunk was in view of a client at the end of the + /// previous tick. + pub fn is_viewed(&self) -> bool { + self.viewed.load(Ordering::Relaxed) + } + + /// Like [`Self::is_viewed`], but avoids an atomic load. + pub fn is_viewed_mut(&mut self) -> bool { + *self.viewed.get_mut() + } + + /// Marks this chunk as being seen by a client. + pub(crate) fn mark_viewed(&self) { + self.viewed.store(true, Ordering::Relaxed); + } + + pub(super) fn into_unloaded(mut self) -> Chunk { + self.cached_init_packets.get_mut().clear(); + + for sect in &mut self.sections { + sect.section_updates.clear(); + } + + Chunk { + sections: self.sections, + cached_init_packets: self.cached_init_packets, + refresh: true, + viewed: AtomicBool::new(false), + } + } + + pub(super) fn write_update_packets( + &mut self, + mut writer: impl WritePacket, + scratch: &mut Vec, + pos: ChunkPos, + info: &InstanceInfo, + ) { + if self.refresh { + self.write_init_packets(info, pos, writer, scratch) + } else { + for (sect_y, sect) in &mut self.sections.iter_mut().enumerate() { + if sect.section_updates.len() == 1 { + let packed = sect.section_updates[0].0 as u64; + let offset_y = packed & 0b1111; + let offset_z = (packed >> 4) & 0b1111; + let offset_x = (packed >> 8) & 0b1111; + let block = packed >> 12; + + let global_x = pos.x * 16 + offset_x as i32; + let global_y = info.min_y + sect_y as i32 * 16 + offset_y as i32; + let global_z = pos.z * 16 + offset_z as i32; + + writer.write_packet(&BlockUpdate { + position: BlockPos::new(global_x, global_y, global_z), + block_id: VarInt(block as i32), + }) + } else if sect.section_updates.len() > 1 { + let chunk_section_position = (pos.x as i64) << 42 + | (pos.z as i64 & 0x3fffff) << 20 + | (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff; + + writer.write_packet(&UpdateSectionBlocksEncode { + chunk_section_position, + invert_trust_edges: false, + blocks: §.section_updates, + }); + } + } + } + } + + /// Writes the chunk data packet for this chunk with the given position. + /// This will initialize the chunk for the client. + pub(crate) fn write_init_packets( + &self, + info: &InstanceInfo, + pos: ChunkPos, + mut writer: impl WritePacket, + scratch: &mut Vec, + ) { + let mut lck = self.cached_init_packets.lock(); + + if lck.is_empty() { + scratch.clear(); + + for sect in &self.sections { + sect.non_air_count.encode(&mut *scratch).unwrap(); + + sect.block_states + .encode_mc_format( + &mut *scratch, + |b| b.to_raw().into(), + 4, + 8, + bit_width(BlockState::max_raw().into()), + ) + .expect("failed to encode block paletted container"); + + sect.biomes + .encode_mc_format( + &mut *scratch, + |b| b.0.into(), + 0, + 3, + bit_width(info.biome_registry_len - 1), + ) + .expect("failed to encode biome paletted container"); + } + + let mut compression_scratch = vec![]; + + let mut writer = PacketWriter::new( + &mut lck, + info.compression_threshold, + &mut compression_scratch, + ); + + writer.write_packet(&ChunkDataAndUpdateLightEncode { + chunk_x: pos.x, + chunk_z: pos.z, + heightmaps: &compound! { + // TODO: MOTION_BLOCKING heightmap + }, + blocks_and_biomes: scratch, + block_entities: &[], + trust_edges: true, + sky_light_mask: &info.filler_sky_light_mask, + block_light_mask: &[], + empty_sky_light_mask: &[], + empty_block_light_mask: &[], + sky_light_arrays: &info.filler_sky_light_arrays, + block_light_arrays: &[], + }); + } + + writer.write_packet_bytes(&lck); + } + + pub(super) fn update_post_client(&mut self) { + self.refresh = false; + + for sect in &mut self.sections { + sect.section_updates.clear(); + } + } +} + +impl Chunk { + /// Returns the number of sections in this chunk. To get the height of the + /// chunk in meters, multiply the result by 16. + pub fn section_count(&self) -> usize { + self.sections.len() + } + + /// Gets the block state at the provided offsets in the chunk. + /// + /// **Note**: The arguments to this function are offsets from the minimum + /// corner of the chunk in _chunk space_ rather than _world space_. + /// + /// # Panics + /// + /// Panics if the offsets are outside the bounds of the chunk. `x` and `z` + /// must be less than 16 while `y` must be less than `section_count() * 16`. + #[track_caller] + pub fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState { + assert!( + x < 16 && y < self.section_count() * 16 && z < 16, + "chunk block offsets of ({x}, {y}, {z}) are out of bounds" + ); + + self.sections[y / 16] + .block_states + .get(x + z * 16 + y % 16 * 16 * 16) + } + + /// Sets the block state at the provided offsets in the chunk. The previous + /// block state at the position is returned. + /// + /// **Note**: The arguments to this function are offsets from the minimum + /// corner of the chunk in _chunk space_ rather than _world space_. + /// + /// # Panics + /// + /// Panics if the offsets are outside the bounds of the chunk. `x` and `z` + /// must be less than 16 while `y` must be less than `section_count() * 16`. + #[track_caller] + pub fn set_block_state( + &mut self, + x: usize, + y: usize, + z: usize, + block: BlockState, + ) -> BlockState { + assert!( + x < 16 && y < self.section_count() * 16 && z < 16, + "chunk block offsets of ({x}, {y}, {z}) are out of bounds" + ); + + let sect_y = y / 16; + let sect = &mut self.sections[sect_y]; + let idx = x + z * 16 + y % 16 * 16 * 16; + + let old_block = sect.block_states.set(idx, block); + + if block != old_block { + // Update non-air count. + match (block.is_air(), old_block.is_air()) { + (true, false) => sect.non_air_count -= 1, + (false, true) => sect.non_air_count += 1, + _ => {} + } + + if LOADED && !self.refresh { + self.cached_init_packets.get_mut().clear(); + let compact = (block.to_raw() as i64) << 12 | (x << 8 | z << 4 | (y % 16)) as i64; + sect.section_updates.push(VarLong(compact)); + } + } + + old_block + } + + /// Sets every block in a section to the given block state. + /// + /// This is semantically equivalent to setting every block in the section + /// with [`set_block_state`]. However, this function may be implemented more + /// efficiently. + /// + /// # Panics + /// + /// Panics if `sect_y` is out of bounds. `sect_y` must be less than the + /// section count. + /// + /// [`set_block_state`]: Self::set_block_state + #[track_caller] + pub fn fill_block_states(&mut self, sect_y: usize, block: BlockState) { + let Some(sect) = self.sections.get_mut(sect_y) else { + panic!( + "section index {sect_y} out of bounds for chunk with {} sections", + self.section_count() + ) + }; + + if LOADED && !self.refresh { + if let PalettedContainer::Single(single) = §.block_states { + if block != *single { + self.cached_init_packets.get_mut().clear(); + + // The whole section is being modified, so any previous modifications would be + // overwritten. + sect.section_updates.clear(); + + // Push section updates for all the blocks in the chunk. + sect.section_updates.reserve_exact(SECTION_BLOCK_COUNT); + let block_bits = (block.to_raw() as i64) << 12; + for z in 0..16 { + for x in 0..16 { + let packed = block_bits | (x << 8 | z << 4 | sect_y as i64); + sect.section_updates.push(VarLong(packed)); + } + } + } + } else { + let block_bits = (block.to_raw() as i64) << 12; + for z in 0..16 { + for x in 0..16 { + let idx = x + z * 16 + sect_y * (16 * 16); + if block != sect.block_states.get(idx) { + self.cached_init_packets.get_mut().clear(); + let packed = block_bits | (x << 8 | z << 4 | sect_y) as i64; + sect.section_updates.push(VarLong(packed)); + } + } + } + } + } + + if !block.is_air() { + sect.non_air_count = SECTION_BLOCK_COUNT as u16; + } else { + sect.non_air_count = 0; + } + + sect.block_states.fill(block); + } + + /// Gets the biome at the provided biome offsets in the chunk. + /// + /// **Note**: the arguments are **not** block positions. Biomes are 4x4x4 + /// segments of a chunk, so `x` and `z` are in `0..4`. + /// + /// # Panics + /// + /// Panics if the offsets are outside the bounds of the chunk. `x` and `z` + /// must be less than 4 while `y` must be less than `section_count() * 4`. + #[track_caller] + pub fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId { + assert!( + x < 4 && y < self.section_count() * 4 && z < 4, + "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" + ); + + self.sections[y / 4].biomes.get(x + z * 4 + y % 4 * 4 * 4) + } + + /// Sets the biome at the provided offsets in the chunk. The previous + /// biome at the position is returned. + /// + /// **Note**: the arguments are **not** block positions. Biomes are 4x4x4 + /// segments of a chunk, so `x` and `z` are in `0..4`. + /// + /// # Panics + /// + /// Panics if the offsets are outside the bounds of the chunk. `x` and `z` + /// must be less than 4 while `y` must be less than `section_count() * 4`. + #[track_caller] + pub fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId { + assert!( + x < 4 && y < self.section_count() * 4 && z < 4, + "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" + ); + + let old_biome = self.sections[y / 4] + .biomes + .set(x + z * 4 + y % 4 * 4 * 4, biome); + + if LOADED && biome != old_biome { + self.cached_init_packets.get_mut().clear(); + self.refresh = true; + } + + old_biome + } + + /// Sets every biome in a section to the given block state. + /// + /// This is semantically equivalent to setting every biome in the section + /// with [`set_biome`]. However, this function may be implemented more + /// efficiently. + /// + /// # Panics + /// + /// Panics if `sect_y` is out of bounds. `sect_y` must be less than the + /// section count. + /// + /// [`set_biome`]: Self::set_biome + #[track_caller] + pub fn fill_biomes(&mut self, sect_y: usize, biome: BiomeId) { + let Some(sect) = self.sections.get_mut(sect_y) else { + panic!( + "section index {sect_y} out of bounds for chunk with {} section(s)", + self.section_count() + ) + }; + + sect.biomes.fill(biome); + + // TODO: this is set unconditionally, but it doesn't have to be. + self.cached_init_packets.get_mut().clear(); + self.refresh = true; + } + + /// Optimizes this chunk to use the minimum amount of memory possible. It + /// has no observable effect on the contents of the chunk. + /// + /// This is a potentially expensive operation. The function is most + /// effective when a large number of blocks and biomes have changed states + /// via [`Self::set_block_state`] and [`Self::set_biome`]. + pub fn optimize(&mut self) { + self.sections.shrink_to_fit(); + self.cached_init_packets.get_mut().shrink_to_fit(); + + for sect in &mut self.sections { + sect.section_updates.shrink_to_fit(); + sect.block_states.optimize(); + sect.biomes.optimize(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::block::BlockState; + + fn check(chunk: &Chunk, total_expected_change_count: usize) { + assert!(!chunk.refresh, "chunk should not be refreshed for the test"); + + let mut change_count = 0; + + for sect in &chunk.sections { + assert_eq!( + (0..SECTION_BLOCK_COUNT) + .filter(|&i| !sect.block_states.get(i).is_air()) + .count(), + sect.non_air_count as usize, + "number of non-air blocks does not match counter" + ); + + change_count += sect.section_updates.len(); + } + + assert_eq!( + change_count, total_expected_change_count, + "bad change count" + ); + } + + #[test] + fn block_state_changes() { + let mut chunk = Chunk::new(5).into_loaded(); + chunk.refresh = false; + + chunk.set_block_state(0, 0, 0, BlockState::SPONGE); + check(&chunk, 1); + chunk.set_block_state(1, 0, 0, BlockState::CAVE_AIR); + check(&chunk, 2); + chunk.set_block_state(2, 0, 0, BlockState::MAGMA_BLOCK); + check(&chunk, 3); + chunk.set_block_state(2, 0, 0, BlockState::MAGMA_BLOCK); + check(&chunk, 3); + + chunk.fill_block_states(0, BlockState::AIR); + check(&chunk, 6); + } +} diff --git a/crates/valence/src/instance/chunk_entry.rs b/crates/valence/src/instance/chunk_entry.rs new file mode 100644 index 0000000..bf85fda --- /dev/null +++ b/crates/valence/src/instance/chunk_entry.rs @@ -0,0 +1,120 @@ +use std::collections::hash_map::{Entry, OccupiedEntry}; + +use super::*; + +#[derive(Debug)] +pub enum ChunkEntry<'a> { + Occupied(OccupiedChunkEntry<'a>), + Vacant(VacantChunkEntry<'a>), +} + +impl<'a> ChunkEntry<'a> { + pub(super) fn new(section_count: usize, entry: Entry<'a, ChunkPos, PartitionCell>) -> Self { + match entry { + Entry::Occupied(oe) => { + if oe.get().chunk.is_some() { + ChunkEntry::Occupied(OccupiedChunkEntry { + section_count, + entry: oe, + }) + } else { + ChunkEntry::Vacant(VacantChunkEntry { + section_count, + entry: Entry::Occupied(oe), + }) + } + } + Entry::Vacant(ve) => ChunkEntry::Vacant(VacantChunkEntry { + section_count, + entry: Entry::Vacant(ve), + }), + } + } + + pub fn or_default(self) -> &'a mut Chunk { + match self { + ChunkEntry::Occupied(oe) => oe.into_mut(), + ChunkEntry::Vacant(ve) => ve.insert(Chunk::default()), + } + } +} + +#[derive(Debug)] +pub struct OccupiedChunkEntry<'a> { + section_count: usize, + entry: OccupiedEntry<'a, ChunkPos, PartitionCell>, +} + +impl<'a> OccupiedChunkEntry<'a> { + pub fn get(&self) -> &Chunk { + self.entry.get().chunk.as_ref().unwrap() + } + + pub fn get_mut(&mut self) -> &mut Chunk { + self.entry.get_mut().chunk.as_mut().unwrap() + } + + pub fn insert(&mut self, mut chunk: Chunk) -> Chunk { + chunk.resize(self.section_count); + + self.entry + .get_mut() + .chunk + .replace(chunk.into_loaded()) + .unwrap() + .into_unloaded() + } + + pub fn into_mut(self) -> &'a mut Chunk { + self.entry.into_mut().chunk.as_mut().unwrap() + } + + pub fn key(&self) -> &ChunkPos { + self.entry.key() + } + + pub fn remove(self) -> Chunk { + let cell = self.entry.into_mut(); + cell.chunk_removed = true; + cell.chunk.take().unwrap().into_unloaded() + } + + pub fn remove_entry(self) -> (ChunkPos, Chunk) { + let pos = *self.entry.key(); + let cell = self.entry.into_mut(); + cell.chunk_removed = true; + (pos, cell.chunk.take().unwrap().into_unloaded()) + } +} + +#[derive(Debug)] +pub struct VacantChunkEntry<'a> { + section_count: usize, + entry: Entry<'a, ChunkPos, PartitionCell>, +} + +impl<'a> VacantChunkEntry<'a> { + pub fn insert(self, mut chunk: Chunk) -> &'a mut Chunk { + chunk.resize(self.section_count); + + let cell = self.entry.or_insert_with(|| PartitionCell { + chunk: None, + chunk_removed: false, + entities: BTreeSet::new(), + incoming: vec![], + outgoing: vec![], + packet_buf: vec![], + }); + + debug_assert!(cell.chunk.is_none()); + cell.chunk.insert(chunk.into_loaded()) + } + + pub fn into_key(self) -> ChunkPos { + *self.entry.key() + } + + pub fn key(&self) -> &ChunkPos { + self.entry.key() + } +} diff --git a/crates/valence/src/chunk/paletted_container.rs b/crates/valence/src/instance/paletted_container.rs similarity index 91% rename from crates/valence/src/chunk/paletted_container.rs rename to crates/valence/src/instance/paletted_container.rs index aa493fb..53357e1 100644 --- a/crates/valence/src/chunk/paletted_container.rs +++ b/crates/valence/src/instance/paletted_container.rs @@ -4,8 +4,7 @@ use std::io::Write; use arrayvec::ArrayVec; use valence_protocol::{Encode, VarInt}; -use crate::chunk::{compact_u64s_len, encode_compact_u64s}; -use crate::util::bit_width; +use crate::math::bit_width; /// `HALF_LEN` must be equal to `ceil(LEN / 2)`. #[derive(Clone, Debug)] @@ -39,7 +38,7 @@ impl } pub fn get(&self, idx: usize) -> T { - self.check_oob(idx); + debug_assert!(idx < LEN); match self { Self::Single(elem) => *elem, @@ -49,7 +48,7 @@ impl } pub fn set(&mut self, idx: usize, val: T) -> T { - self.check_oob(idx); + debug_assert!(idx < LEN); match self { Self::Single(old_val) => { @@ -126,14 +125,6 @@ impl } } - #[inline] - fn check_oob(&self, idx: usize) { - assert!( - idx < LEN, - "index {idx} is out of bounds in paletted container of length {LEN}" - ); - } - /// Encodes the paletted container in the format that Minecraft expects. /// /// - **`writer`**: The [`Write`] instance to write the paletted container @@ -264,6 +255,37 @@ impl Indirect usize { + let vals_per_u64 = 64 / bits_per_val; + num::Integer::div_ceil(&vals_count, &vals_per_u64) +} + +#[inline] +fn encode_compact_u64s( + mut w: impl Write, + mut vals: impl Iterator, + bits_per_val: usize, +) -> anyhow::Result<()> { + debug_assert!(bits_per_val <= 64); + + let vals_per_u64 = 64 / bits_per_val; + + loop { + let mut n = 0; + for i in 0..vals_per_u64 { + match vals.next() { + Some(val) => { + debug_assert!(val < 2_u128.pow(bits_per_val as _) as _); + n |= val << (i * bits_per_val); + } + None if i > 0 => return n.encode(&mut w), + None => return Ok(()), + } + } + n.encode(&mut w)?; + } +} + #[cfg(test)] mod tests { use rand::Rng; diff --git a/crates/valence/src/inventory.rs b/crates/valence/src/inventory.rs index 9d0a180..43246b4 100644 --- a/crates/valence/src/inventory.rs +++ b/crates/valence/src/inventory.rs @@ -1,99 +1,18 @@ use std::iter::FusedIterator; -use std::mem; -use std::num::Wrapping; -use std::ops::{Deref, DerefMut, Index, IndexMut}; -use valence_protocol::packets::s2c::play::SetContainerSlotEncode; -use valence_protocol::{InventoryKind, ItemStack, Text, VarInt}; +use bevy_ecs::prelude::*; +use tracing::{debug, warn}; +use valence_protocol::packets::s2c::play::{ + CloseContainerS2c, OpenScreen, SetContainerContentEncode, SetContainerSlotEncode, +}; +use valence_protocol::types::{GameMode, WindowType}; +use valence_protocol::{ItemStack, Text, VarInt}; -use crate::config::Config; -use crate::server::PlayPacketSender; -use crate::slab_versioned::{Key, VersionedSlab}; +use crate::client::event::{ClickContainer, CloseContainer, SetCreativeModeSlot, SetHeldItem}; +use crate::client::Client; -pub struct Inventories { - slab: VersionedSlab>, -} - -impl Inventories { - pub(crate) fn new() -> Self { - Self { - slab: VersionedSlab::new(), - } - } - - pub fn insert( - &mut self, - kind: InventoryKind, - title: impl Into, - state: C::InventoryState, - ) -> (InventoryId, &mut Inventory) { - let (id, inv) = self.slab.insert(Inventory { - state, - title: title.into(), - kind, - slots: vec![None; kind.slot_count()].into(), - modified: 0, - }); - - (InventoryId(id), inv) - } - - pub fn remove(&mut self, id: InventoryId) -> Option { - self.slab.remove(id.0).map(|inv| inv.state) - } - - pub fn get(&self, id: InventoryId) -> Option<&Inventory> { - self.slab.get(id.0) - } - - pub fn get_mut(&mut self, id: InventoryId) -> Option<&mut Inventory> { - self.slab.get_mut(id.0) - } - - pub fn iter( - &self, - ) -> impl ExactSizeIterator)> + FusedIterator + Clone + '_ - { - self.slab.iter().map(|(k, inv)| (InventoryId(k), inv)) - } - - pub fn iter_mut( - &mut self, - ) -> impl ExactSizeIterator)> + FusedIterator + '_ { - self.slab.iter_mut().map(|(k, inv)| (InventoryId(k), inv)) - } - - pub(crate) fn update(&mut self) { - for (_, inv) in self.iter_mut() { - inv.modified = 0; - } - } -} - -impl Index for Inventories { - type Output = Inventory; - - fn index(&self, index: InventoryId) -> &Self::Output { - self.get(index).expect("invalid inventory ID") - } -} - -impl IndexMut for Inventories { - fn index_mut(&mut self, index: InventoryId) -> &mut Self::Output { - self.get_mut(index).expect("invalid inventory ID") - } -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub struct InventoryId(Key); - -impl InventoryId { - pub const NULL: Self = Self(Key::NULL); -} - -pub struct Inventory { - /// Custom state - pub state: C::InventoryState, +#[derive(Debug, Clone, Component)] +pub struct Inventory { title: Text, kind: InventoryKind, slots: Box<[Option]>, @@ -101,21 +20,22 @@ pub struct Inventory { modified: u64, } -impl Deref for Inventory { - type Target = C::InventoryState; - - fn deref(&self) -> &Self::Target { - &self.state +impl Inventory { + pub fn new(kind: InventoryKind) -> Self { + // TODO: default title to the correct translation key instead + Self::with_title(kind, "Inventory") } -} -impl DerefMut for Inventory { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.state + pub fn with_title(kind: InventoryKind, title: impl Into) -> Self { + Inventory { + title: title.into(), + kind, + slots: vec![None; kind.slot_count()].into(), + modified: 0, + } } -} -impl Inventory { + #[track_caller] pub fn slot(&self, idx: u16) -> Option<&ItemStack> { self.slots .get(idx as usize) @@ -123,6 +43,7 @@ impl Inventory { .as_ref() } + #[track_caller] pub fn replace_slot( &mut self, idx: u16, @@ -137,9 +58,10 @@ impl Inventory { self.modified |= 1 << idx; } - mem::replace(old, new) + std::mem::replace(old, new) } + #[track_caller] pub fn swap_slot(&mut self, idx_a: u16, idx_b: u16) { assert!(idx_a < self.slot_count(), "slot index out of range"); assert!(idx_b < self.slot_count(), "slot index out of range"); @@ -179,34 +101,987 @@ impl Inventory { pub fn replace_title(&mut self, title: impl Into) -> Text { // TODO: set title modified flag - mem::replace(&mut self.title, title.into()) + std::mem::replace(&mut self.title, title.into()) } - pub(crate) fn slot_slice(&self) -> &[Option] { + fn slot_slice(&self) -> &[Option] { self.slots.as_ref() } +} - pub(crate) fn send_update( - &self, - send: &mut PlayPacketSender, - window_id: u8, - state_id: &mut Wrapping, - ) -> anyhow::Result<()> { - if self.modified != 0 { - for (idx, slot) in self.slots.iter().enumerate() { - if (self.modified >> idx) & 1 == 1 { - *state_id += 1; +/// Send updates for each client's player inventory. +pub(crate) fn update_player_inventories( + mut query: Query<(&mut Inventory, &mut Client), Without>, +) { + for (mut inventory, mut client) in query.iter_mut() { + if inventory.kind != InventoryKind::Player { + warn!("Inventory on client entity is not a player inventory"); + } - send.append_packet(&SetContainerSlotEncode { - window_id: window_id as i8, - state_id: VarInt(state_id.0), - slot_idx: idx as i16, - slot_data: slot.as_ref(), - })?; + if inventory.modified != 0 { + if inventory.modified == u64::MAX { + // Update the whole inventory. + client.inventory_state_id += 1; + let cursor_item = client.cursor_item.clone(); + let state_id = client.inventory_state_id.0; + client.write_packet(&SetContainerContentEncode { + window_id: 0, + state_id: VarInt(state_id), + slots: inventory.slot_slice(), + carried_item: &cursor_item, + }); + + client.cursor_item_modified = false; + } else { + // send the modified slots + + // The slots that were NOT modified by this client, and they need to be sent + let modified_filtered = inventory.modified & !client.inventory_slots_modified; + if modified_filtered != 0 { + client.inventory_state_id += 1; + let state_id = client.inventory_state_id.0; + for (i, slot) in inventory.slots.iter().enumerate() { + if ((modified_filtered >> i) & 1) == 1 { + client.write_packet(&SetContainerSlotEncode { + window_id: 0, + state_id: VarInt(state_id), + slot_idx: i as i16, + slot_data: slot.as_ref(), + }); + } + } + } + } + + inventory.modified = 0; + client.inventory_slots_modified = 0; + } + + if client.cursor_item_modified { + client.inventory_state_id += 1; + + client.cursor_item_modified = false; + + let cursor_item = client.cursor_item.clone(); + let state_id = client.inventory_state_id.0; + client.write_packet(&SetContainerSlotEncode { + window_id: -1, + state_id: VarInt(state_id), + slot_idx: -1, + slot_data: cursor_item.as_ref(), + }); + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum InventoryKind { + Generic9x1, + Generic9x2, + Generic9x3, + Generic9x4, + Generic9x5, + Generic9x6, + Generic3x3, + Anvil, + Beacon, + BlastFurnace, + BrewingStand, + Crafting, + Enchantment, + Furnace, + Grindstone, + Hopper, + Lectern, + Loom, + Merchant, + ShulkerBox, + Smithing, + Smoker, + Cartography, + Stonecutter, + Player, +} + +impl InventoryKind { + /// The number of slots in this inventory. When the inventory is shown to + /// clients, this number does not include the player's main inventory slots. + pub const fn slot_count(self) -> usize { + match self { + InventoryKind::Generic9x1 => 9, + InventoryKind::Generic9x2 => 9 * 2, + InventoryKind::Generic9x3 => 9 * 3, + InventoryKind::Generic9x4 => 9 * 4, + InventoryKind::Generic9x5 => 9 * 5, + InventoryKind::Generic9x6 => 9 * 6, + InventoryKind::Generic3x3 => 3 * 3, + InventoryKind::Anvil => 4, + InventoryKind::Beacon => 1, + InventoryKind::BlastFurnace => 3, + InventoryKind::BrewingStand => 5, + InventoryKind::Crafting => 10, + InventoryKind::Enchantment => 2, + InventoryKind::Furnace => 3, + InventoryKind::Grindstone => 3, + InventoryKind::Hopper => 5, + InventoryKind::Lectern => 1, + InventoryKind::Loom => 4, + InventoryKind::Merchant => 3, + InventoryKind::ShulkerBox => 27, + InventoryKind::Smithing => 3, + InventoryKind::Smoker => 3, + InventoryKind::Cartography => 3, + InventoryKind::Stonecutter => 2, + InventoryKind::Player => 46, + } + } +} + +impl From for WindowType { + fn from(value: InventoryKind) -> Self { + match value { + InventoryKind::Generic9x1 => WindowType::Generic9x1, + InventoryKind::Generic9x2 => WindowType::Generic9x2, + InventoryKind::Generic9x3 => WindowType::Generic9x3, + InventoryKind::Generic9x4 => WindowType::Generic9x4, + InventoryKind::Generic9x5 => WindowType::Generic9x5, + InventoryKind::Generic9x6 => WindowType::Generic9x6, + InventoryKind::Generic3x3 => WindowType::Generic3x3, + InventoryKind::Anvil => WindowType::Anvil, + InventoryKind::Beacon => WindowType::Beacon, + InventoryKind::BlastFurnace => WindowType::BlastFurnace, + InventoryKind::BrewingStand => WindowType::BrewingStand, + InventoryKind::Crafting => WindowType::Crafting, + InventoryKind::Enchantment => WindowType::Enchantment, + InventoryKind::Furnace => WindowType::Furnace, + InventoryKind::Grindstone => WindowType::Grindstone, + InventoryKind::Hopper => WindowType::Hopper, + InventoryKind::Lectern => WindowType::Lectern, + InventoryKind::Loom => WindowType::Loom, + InventoryKind::Merchant => WindowType::Merchant, + InventoryKind::ShulkerBox => WindowType::ShulkerBox, + InventoryKind::Smithing => WindowType::Smithing, + InventoryKind::Smoker => WindowType::Smoker, + InventoryKind::Cartography => WindowType::Cartography, + InventoryKind::Stonecutter => WindowType::Stonecutter, + // arbitrarily chosen, because a player inventory technically does not have a window + // type + InventoryKind::Player => WindowType::Generic9x4, + } + } +} + +impl From for InventoryKind { + fn from(value: WindowType) -> Self { + match value { + WindowType::Generic9x1 => InventoryKind::Generic9x1, + WindowType::Generic9x2 => InventoryKind::Generic9x2, + WindowType::Generic9x3 => InventoryKind::Generic9x3, + WindowType::Generic9x4 => InventoryKind::Generic9x4, + WindowType::Generic9x5 => InventoryKind::Generic9x5, + WindowType::Generic9x6 => InventoryKind::Generic9x6, + WindowType::Generic3x3 => InventoryKind::Generic3x3, + WindowType::Anvil => InventoryKind::Anvil, + WindowType::Beacon => InventoryKind::Beacon, + WindowType::BlastFurnace => InventoryKind::BlastFurnace, + WindowType::BrewingStand => InventoryKind::BrewingStand, + WindowType::Crafting => InventoryKind::Crafting, + WindowType::Enchantment => InventoryKind::Enchantment, + WindowType::Furnace => InventoryKind::Furnace, + WindowType::Grindstone => InventoryKind::Grindstone, + WindowType::Hopper => InventoryKind::Hopper, + WindowType::Lectern => InventoryKind::Lectern, + WindowType::Loom => InventoryKind::Loom, + WindowType::Merchant => InventoryKind::Merchant, + WindowType::ShulkerBox => InventoryKind::ShulkerBox, + WindowType::Smithing => InventoryKind::Smithing, + WindowType::Smoker => InventoryKind::Smoker, + WindowType::Cartography => InventoryKind::Cartography, + WindowType::Stonecutter => InventoryKind::Stonecutter, + } + } +} + +/// Used to indicate that the client with this component is currently viewing +/// an inventory. +#[derive(Debug, Clone, Component)] +pub struct OpenInventory { + /// The Entity with the `Inventory` component that the client is currently + /// viewing. + pub(crate) entity: Entity, + client_modified: u64, +} + +impl OpenInventory { + pub fn new(entity: Entity) -> Self { + OpenInventory { + entity, + client_modified: 0, + } + } + + pub fn entity(&self) -> Entity { + self.entity + } +} + +/// Handles the `OpenInventory` component being added to a client, which +/// indicates that the client is now viewing an inventory, and sends inventory +/// updates to the client when the inventory is modified. +pub(crate) fn update_open_inventories( + mut commands: Commands, + mut clients: Query<(Entity, &mut Client, &mut OpenInventory)>, + mut inventories: Query<&mut Inventory>, +) { + // These operations need to happen in this order. + + // send the inventory contents to all clients that are viewing an inventory + for (client_entity, mut client, mut open_inventory) in clients.iter_mut() { + // validate that the inventory exists + let Ok(inventory) = inventories.get_component::(open_inventory.entity) else { + // the inventory no longer exists, so close the inventory + commands.entity(client_entity).remove::(); + let window_id = client.window_id; + client.write_packet(&CloseContainerS2c { + window_id, + }); + continue; + }; + + if open_inventory.is_added() { + // send the inventory to the client if the client just opened the inventory + client.window_id = client.window_id % 100 + 1; + open_inventory.client_modified = 0; + + let packet = OpenScreen { + window_id: VarInt(client.window_id.into()), + window_type: WindowType::from(inventory.kind), + window_title: (&inventory.title).into(), + }; + client.write_packet(&packet); + + let packet = SetContainerContentEncode { + window_id: client.window_id, + state_id: VarInt(client.inventory_state_id.0), + slots: inventory.slot_slice(), + carried_item: &client.cursor_item.clone(), + }; + client.write_packet(&packet); + } else { + // the client is already viewing the inventory + if inventory.modified == u64::MAX { + // send the entire inventory + client.inventory_state_id += 1; + let packet = SetContainerContentEncode { + window_id: client.window_id, + state_id: VarInt(client.inventory_state_id.0), + slots: inventory.slot_slice(), + carried_item: &client.cursor_item.clone(), + }; + client.write_packet(&packet); + } else { + // send the modified slots + let window_id = client.window_id as i8; + // The slots that were NOT modified by this client, and they need to be sent + let modified_filtered = inventory.modified & !open_inventory.client_modified; + if modified_filtered != 0 { + client.inventory_state_id += 1; + let state_id = client.inventory_state_id.0; + for (i, slot) in inventory.slots.iter().enumerate() { + if (modified_filtered >> i) & 1 == 1 { + client.write_packet(&SetContainerSlotEncode { + window_id, + state_id: VarInt(state_id), + slot_idx: i as i16, + slot_data: slot.as_ref(), + }); + } + } } } } + open_inventory.client_modified = 0; + client.inventory_slots_modified = 0; + } + + // reset the modified flag + for (_, _, open_inventory) in clients.iter_mut() { + // validate that the inventory exists + if let Ok(mut inventory) = inventories.get_component_mut::(open_inventory.entity) + { + inventory.modified = 0; + } + } +} + +/// Handles clients telling the server that they are closing an inventory. +pub(crate) fn handle_close_container( + mut commands: Commands, + mut events: EventReader, +) { + for event in events.iter() { + commands.entity(event.client).remove::(); + } +} + +/// Detects when a client's `OpenInventory` component is removed, which +/// indicates that the client is no longer viewing an inventory. +pub(crate) fn update_client_on_close_inventory( + removals: RemovedComponents, + mut clients: Query<&mut Client>, +) { + for entity in removals.iter() { + if let Ok(mut client) = clients.get_component_mut::(entity) { + let window_id = client.window_id; + client.write_packet(&CloseContainerS2c { window_id }); + } + } +} + +pub(crate) fn handle_click_container( + mut clients: Query<(&mut Client, &mut Inventory, Option<&mut OpenInventory>)>, + mut inventories: Query<&mut Inventory, Without>, + mut events: EventReader, +) { + for event in events.iter() { + let Ok((mut client, mut client_inventory, mut open_inventory)) = + clients.get_mut(event.client) else { + // the client does not exist, ignore + continue; + }; + + // validate the window id + if (event.window_id == 0) != open_inventory.is_none() { + warn!( + "Client sent a click with an invalid window id for current state: window_id = {}, \ + open_inventory present = {}", + event.window_id, + open_inventory.is_some() + ); + continue; + } + + if let Some(open_inventory) = open_inventory.as_mut() { + // the player is interacting with an inventory that is open + let Ok(mut target_inventory) = inventories.get_component_mut::(open_inventory.entity) else { + // the inventory does not exist, ignore + continue; + }; + if client.inventory_state_id.0 != event.state_id { + // client is out of sync, resync, ignore click + debug!("Client state id mismatch, resyncing"); + client.inventory_state_id += 1; + let packet = SetContainerContentEncode { + window_id: client.window_id, + state_id: VarInt(client.inventory_state_id.0), + slots: target_inventory.slot_slice(), + carried_item: &client.cursor_item.clone(), + }; + client.write_packet(&packet); + continue; + } + + client.cursor_item = event.carried_item.clone(); + + for (slot_id, item) in event.slot_changes.clone() { + if (0i16..target_inventory.slot_count() as i16).contains(&slot_id) { + // the client is interacting with a slot in the target inventory + target_inventory.replace_slot(slot_id as u16, item); + open_inventory.client_modified |= 1 << slot_id; + } else { + // the client is interacting with a slot in their own inventory + let slot_id = convert_to_player_slot_id(target_inventory.kind, slot_id as u16); + client_inventory.replace_slot(slot_id, item); + client.inventory_slots_modified |= 1 << slot_id; + } + } + } else { + // the client is interacting with their own inventory + + if client.inventory_state_id.0 != event.state_id { + // client is out of sync, resync, and ignore the click + debug!("Client state id mismatch, resyncing"); + client.inventory_state_id += 1; + let packet = SetContainerContentEncode { + window_id: client.window_id, + state_id: VarInt(client.inventory_state_id.0), + slots: client_inventory.slot_slice(), + carried_item: &client.cursor_item.clone(), + }; + client.write_packet(&packet); + continue; + } + + // TODO: do more validation on the click + client.cursor_item = event.carried_item.clone(); + for (slot_id, item) in event.slot_changes.clone() { + if (0i16..client_inventory.slot_count() as i16).contains(&slot_id) { + client_inventory.replace_slot(slot_id as u16, item); + client.inventory_slots_modified |= 1 << slot_id; + } else { + // the client is trying to interact with a slot that does not exist, + // ignore + warn!( + "Client attempted to interact with slot {} which does not exist", + slot_id + ); + } + } + } + } +} + +pub(crate) fn handle_set_slot_creative( + mut clients: Query<(&mut Client, &mut Inventory)>, + mut events: EventReader, +) { + for event in events.iter() { + if let Ok((mut client, mut inventory)) = clients.get_mut(event.client) { + if client.game_mode() != GameMode::Creative { + // the client is not in creative mode, ignore + continue; + } + inventory.replace_slot(event.slot as u16, event.clicked_item.clone()); + inventory.modified &= !(1 << event.slot); // clear the modified bit, since we are about to send the update + client.inventory_state_id += 1; + let state_id = client.inventory_state_id.0; + // HACK: notchian clients rely on the server to send the slot update when in + // creative mode Simply marking the slot as modified is not enough. This was + // discovered because shift-clicking the destroy item slot in creative mode does + // not work without this hack. + client.write_packet(&SetContainerSlotEncode { + window_id: 0, + state_id: VarInt(state_id), + slot_idx: event.slot, + slot_data: event.clicked_item.as_ref(), + }); + } + } +} + +pub(crate) fn handle_set_held_item( + mut clients: Query<&mut Client>, + mut events: EventReader, +) { + for event in events.iter() { + if let Ok(mut client) = clients.get_mut(event.client) { + client.held_item_slot = convert_hotbar_slot_id(event.slot as u16); + } + } +} + +/// Convert a slot that is outside a target inventory's range to a slot that is +/// inside the player's inventory. +fn convert_to_player_slot_id(target_kind: InventoryKind, slot_id: u16) -> u16 { + // the first slot in the player's general inventory + let offset = target_kind.slot_count() as u16; + slot_id - offset + 9 +} + +fn convert_hotbar_slot_id(slot_id: u16) -> u16 { + slot_id + 36 +} + +#[cfg(test)] +mod test { + use bevy_app::App; + use valence_protocol::packets::S2cPlayPacket; + use valence_protocol::ItemKind; + + use super::*; + use crate::unit_test::util::scenario_single_client; + use crate::{assert_packet_count, assert_packet_order}; + + #[test] + fn test_convert_to_player_slot() { + assert_eq!(convert_to_player_slot_id(InventoryKind::Generic9x3, 27), 9); + assert_eq!(convert_to_player_slot_id(InventoryKind::Generic9x3, 36), 18); + assert_eq!(convert_to_player_slot_id(InventoryKind::Generic9x3, 54), 36); + assert_eq!(convert_to_player_slot_id(InventoryKind::Generic9x1, 9), 9); + } + + #[test] + fn test_convert_hotbar_slot_id() { + assert_eq!(convert_hotbar_slot_id(0), 36); + assert_eq!(convert_hotbar_slot_id(4), 40); + assert_eq!(convert_hotbar_slot_id(8), 44); + } + + #[test] + fn test_should_open_inventory() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + + let inventory = Inventory::new(InventoryKind::Generic3x3); + let inventory_ent = app.world.spawn(inventory).id(); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Open the inventory. + let open_inventory = OpenInventory::new(inventory_ent); + app.world + .get_entity_mut(client_ent) + .expect("could not find client") + .insert(open_inventory); + + app.update(); + + // Make assertions + let sent_packets = client_helper.collect_sent()?; + + assert_packet_count!(sent_packets, 1, S2cPlayPacket::OpenScreen(_)); + assert_packet_count!(sent_packets, 1, S2cPlayPacket::SetContainerContent(_)); + assert_packet_order!( + sent_packets, + S2cPlayPacket::OpenScreen(_), + S2cPlayPacket::SetContainerContent(_) + ); + + Ok(()) + } + + #[test] + fn test_should_close_inventory() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + + let inventory = Inventory::new(InventoryKind::Generic3x3); + let inventory_ent = app.world.spawn(inventory).id(); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Open the inventory. + let open_inventory = OpenInventory::new(inventory_ent); + app.world + .get_entity_mut(client_ent) + .expect("could not find client") + .insert(open_inventory); + + app.update(); + client_helper.clear_sent(); + + // Close the inventory. + app.world + .get_entity_mut(client_ent) + .expect("could not find client") + .remove::(); + + app.update(); + + // Make assertions + let sent_packets = client_helper.collect_sent()?; + + assert_packet_count!(sent_packets, 1, S2cPlayPacket::CloseContainerS2c(_)); + + Ok(()) + } + + #[test] + fn test_should_remove_invalid_open_inventory() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + + let inventory = Inventory::new(InventoryKind::Generic3x3); + let inventory_ent = app.world.spawn(inventory).id(); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Open the inventory. + let open_inventory = OpenInventory::new(inventory_ent); + app.world + .get_entity_mut(client_ent) + .expect("could not find client") + .insert(open_inventory); + + app.update(); + client_helper.clear_sent(); + + // Remove the inventory. + app.world.despawn(inventory_ent); + + app.update(); + + // Make assertions + assert!(app.world.get::(client_ent).is_none()); + let sent_packets = client_helper.collect_sent()?; + assert_packet_count!(sent_packets, 1, S2cPlayPacket::CloseContainerS2c(_)); + + Ok(()) + } + + #[test] + fn test_should_modify_player_inventory_click_container() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let mut inventory = app + .world + .get_mut::(client_ent) + .expect("could not find inventory for client"); + inventory.replace_slot(20, ItemStack::new(ItemKind::Diamond, 2, None)); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Make the client click the slot and pick up the item. + let state_id = app + .world + .get::(client_ent) + .unwrap() + .inventory_state_id; + client_helper.send(&valence_protocol::packets::c2s::play::ClickContainer { + window_id: 0, + button: 0, + mode: valence_protocol::types::ClickContainerMode::Click, + state_id: VarInt(state_id.0), + slot_idx: 20, + slots: vec![(20, None)], + carried_item: Some(ItemStack::new(ItemKind::Diamond, 2, None)), + }); + + app.update(); + + // Make assertions + let sent_packets = client_helper.collect_sent()?; + + // because the inventory was modified as a result of the client's click, the + // server should not send any packets to the client because the client + // already knows about the change. + assert_packet_count!( + sent_packets, + 0, + S2cPlayPacket::SetContainerContent(_) | S2cPlayPacket::SetContainerSlot(_) + ); + let inventory = app + .world + .get::(client_ent) + .expect("could not find inventory for client"); + assert_eq!(inventory.slot(20), None); + let client = app + .world + .get::(client_ent) + .expect("could not find client"); + assert_eq!( + client.cursor_item, + Some(ItemStack::new(ItemKind::Diamond, 2, None)) + ); + + Ok(()) + } + + #[test] + fn test_should_modify_player_inventory_server_side() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let mut inventory = app + .world + .get_mut::(client_ent) + .expect("could not find inventory for client"); + inventory.replace_slot(20, ItemStack::new(ItemKind::Diamond, 2, None)); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Modify the inventory. + let mut inventory = app + .world + .get_mut::(client_ent) + .expect("could not find inventory for client"); + inventory.replace_slot(21, ItemStack::new(ItemKind::IronIngot, 1, None)); + + app.update(); + + // Make assertions + let sent_packets = client_helper.collect_sent()?; + // because the inventory was modified server side, the client needs to be + // updated with the change. + assert_packet_count!(sent_packets, 1, S2cPlayPacket::SetContainerSlot(_)); + + Ok(()) + } + + #[test] + fn test_should_sync_entire_player_inventory() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + let mut inventory = app + .world + .get_mut::(client_ent) + .expect("could not find inventory for client"); + inventory.modified = u64::MAX; + + app.update(); + + // Make assertions + let sent_packets = client_helper.collect_sent()?; + assert_packet_count!(sent_packets, 1, S2cPlayPacket::SetContainerContent(_)); + + Ok(()) + } + + fn set_up_open_inventory(app: &mut App, client_ent: Entity) -> Entity { + let inventory = Inventory::new(InventoryKind::Generic9x3); + let inventory_ent = app.world.spawn(inventory).id(); + + // Open the inventory. + let open_inventory = OpenInventory::new(inventory_ent); + app.world + .get_entity_mut(client_ent) + .expect("could not find client") + .insert(open_inventory); + + inventory_ent + } + + #[test] + fn test_should_modify_open_inventory_click_container() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let inventory_ent = set_up_open_inventory(&mut app, client_ent); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Make the client click the slot and pick up the item. + let state_id = app + .world + .get::(client_ent) + .unwrap() + .inventory_state_id; + let window_id = app.world.get::(client_ent).unwrap().window_id; + client_helper.send(&valence_protocol::packets::c2s::play::ClickContainer { + window_id, + button: 0, + mode: valence_protocol::types::ClickContainerMode::Click, + state_id: VarInt(state_id.0), + slot_idx: 20, + slots: vec![(20, None)], + carried_item: Some(ItemStack::new(ItemKind::Diamond, 2, None)), + }); + + app.update(); + + // Make assertions + let sent_packets = client_helper.collect_sent()?; + + // because the inventory was modified as a result of the client's click, the + // server should not send any packets to the client because the client + // already knows about the change. + assert_packet_count!( + sent_packets, + 0, + S2cPlayPacket::SetContainerContent(_) | S2cPlayPacket::SetContainerSlot(_) + ); + let inventory = app + .world + .get::(inventory_ent) + .expect("could not find inventory"); + assert_eq!(inventory.slot(20), None); + let client = app + .world + .get::(client_ent) + .expect("could not find client"); + assert_eq!( + client.cursor_item, + Some(ItemStack::new(ItemKind::Diamond, 2, None)) + ); + + Ok(()) + } + + #[test] + fn test_should_modify_open_inventory_server_side() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let inventory_ent = set_up_open_inventory(&mut app, client_ent); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Modify the inventory. + let mut inventory = app + .world + .get_mut::(inventory_ent) + .expect("could not find inventory for client"); + inventory.replace_slot(5, ItemStack::new(ItemKind::IronIngot, 1, None)); + + app.update(); + + // Make assertions + let sent_packets = client_helper.collect_sent()?; + + // because the inventory was modified server side, the client needs to be + // updated with the change. + assert_packet_count!(sent_packets, 1, S2cPlayPacket::SetContainerSlot(_)); + let inventory = app + .world + .get::(inventory_ent) + .expect("could not find inventory for client"); + assert_eq!( + inventory.slot(5), + Some(&ItemStack::new(ItemKind::IronIngot, 1, None)) + ); + + Ok(()) + } + + #[test] + fn test_should_sync_entire_open_inventory() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let inventory_ent = set_up_open_inventory(&mut app, client_ent); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + let mut inventory = app + .world + .get_mut::(inventory_ent) + .expect("could not find inventory"); + inventory.modified = u64::MAX; + + app.update(); + + // Make assertions + let sent_packets = client_helper.collect_sent()?; + assert_packet_count!(sent_packets, 1, S2cPlayPacket::SetContainerContent(_)); + + Ok(()) + } + + #[test] + fn test_set_creative_mode_slot_handling() { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let mut client = app + .world + .get_mut::(client_ent) + .expect("could not find client"); + client.set_game_mode(GameMode::Creative); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + client_helper.send(&valence_protocol::packets::c2s::play::SetCreativeModeSlot { + slot: 36, + clicked_item: Some(ItemStack::new(ItemKind::Diamond, 2, None)), + }); + + app.update(); + + // Make assertions + let inventory = app + .world + .get::(client_ent) + .expect("could not find inventory for client"); + assert_eq!( + inventory.slot(36), + Some(&ItemStack::new(ItemKind::Diamond, 2, None)) + ); + } + + #[test] + fn test_ignore_set_creative_mode_slot_if_not_creative() { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let mut client = app + .world + .get_mut::(client_ent) + .expect("could not find client"); + client.set_game_mode(GameMode::Survival); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + client_helper.send(&valence_protocol::packets::c2s::play::SetCreativeModeSlot { + slot: 36, + clicked_item: Some(ItemStack::new(ItemKind::Diamond, 2, None)), + }); + + app.update(); + + // Make assertions + let inventory = app + .world + .get::(client_ent) + .expect("could not find inventory for client"); + assert_eq!(inventory.slot(36), None); + } + + #[test] + fn test_window_id_increments() { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let inventory = Inventory::new(InventoryKind::Generic9x3); + let inventory_ent = app.world.spawn(inventory).id(); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + for _ in 0..3 { + let open_inventory = OpenInventory::new(inventory_ent); + app.world + .get_entity_mut(client_ent) + .expect("could not find client") + .insert(open_inventory); + + app.update(); + + app.world + .get_entity_mut(client_ent) + .expect("could not find client") + .remove::(); + + app.update(); + } + + // Make assertions + let client = app + .world + .get::(client_ent) + .expect("could not find client"); + assert_eq!(client.window_id, 3); + } + + #[test] + fn test_should_handle_set_held_item() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + client_helper.send(&valence_protocol::packets::c2s::play::SetHeldItemC2s { slot: 4 }); + + app.update(); + + // Make assertions + let client = app + .world + .get::(client_ent) + .expect("could not find client"); + assert_eq!(client.held_item_slot, 40); + Ok(()) } } diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index 9d5d06b..d707dcc 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -1,79 +1,17 @@ -//! -//! -//! --- -//! -//! A Rust framework for building Minecraft servers. -//! -//! At a high level, a Valence [`Server`] is a collection of [`Clients`], -//! [`Entities`], and [`Worlds`]. When a client connects to the server they are -//! added to the collection of `Clients`. After connecting, clients should -//! be assigned to a [`World`] where they can interact with the entities -//! and [`Chunks`] that are a part of it. -//! -//! The Valence documentation assumes some familiarity with Minecraft and its -//! mechanics. See the [Minecraft Wiki] for general information and [wiki.vg] -//! for protocol documentation. -//! -//! For more information, see the repository [README]. -//! -//! [Minecraft Wiki]: https://minecraft.fandom.com/wiki/Minecraft_Wiki -//! [wiki.vg]: https://wiki.vg/Main_Page -//! [README]: https://github.com/rj00a/valence -//! -//! # Logging -//! -//! Valence uses the [log] crate to report errors and other information. You may -//! want to use a logging implementation such as [env_logger] to see these -//! messages. -//! -//! [log]: https://docs.rs/log/latest/log/ -//! [env_logger]: https://docs.rs/env_logger/latest/env_logger/ -//! -//! # An Important Note on [`mem::swap`] -//! -//! In Valence, many types are owned by the library but given out as mutable -//! references for the user to modify. Examples of such types include [`World`], -//! [`LoadedChunk`], [`Entity`], and [`Client`]. -//! -//! **You must not call [`mem::swap`] on these references (or any other -//! function that would move their location in memory).** Doing so breaks -//! invariants within the library and the resulting behavior is safe but -//! unspecified. You can think of these types as being [pinned](std::pin). -//! -//! Preventing this illegal behavior using Rust's type system was considered too -//! cumbersome, so this note has been left here instead. -//! -//! [`mem::swap`]: std::mem::swap -//! -//! # Examples -//! -//! See the [examples] directory in the source repository. -//! -//! [examples]: https://github.com/rj00a/valence/tree/main/examples -//! -//! [`Server`]: crate::server::Server -//! [`Clients`]: crate::client::Clients -//! [`Entities`]: crate::entity::Entities -//! [`Worlds`]: crate::world::Worlds -//! [`World`]: crate::world::World -//! [`Chunks`]: crate::chunk::Chunks -//! [`LoadedChunk`]: crate::chunk::LoadedChunk -//! [`Entity`]: crate::entity::Entity -//! [`Client`]: crate::client::Client +//! Valence is a Minecraft server framework written in Rust. #![doc( html_logo_url = "https://raw.githubusercontent.com/valence-rs/valence/main/assets/logo.svg", html_favicon_url = "https://raw.githubusercontent.com/valence-rs/valence/main/assets/logo.svg" )] -#![forbid(unsafe_code)] -// Deny these to make CI checks fail. TODO: invalid_html_tags #![deny( rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links, rustdoc::missing_crate_level_docs, rustdoc::invalid_codeblock_attributes, rustdoc::invalid_rust_codeblocks, - rustdoc::bare_urls + rustdoc::bare_urls, + rustdoc::invalid_html_tags )] #![warn( trivial_casts, @@ -88,89 +26,77 @@ clippy::comparison_chain )] -/// Used on [`Config`](config::Config) to allow for async methods in traits. -/// -/// For more information see the [async_trait] crate. -/// -/// [async_trait]: https://docs.rs/async-trait/latest/async_trait/ -pub use async_trait::async_trait; -#[doc(inline)] -pub use server::start_server; -pub use valence_protocol as protocol; -#[doc(inline)] -pub use {uuid, valence_nbt as nbt, vek}; +use bevy_ecs::prelude::*; +pub use { + anyhow, async_trait, bevy_app, bevy_ecs, uuid, valence_nbt as nbt, valence_protocol as protocol, +}; pub mod biome; -pub mod chunk; pub mod client; pub mod config; pub mod dimension; pub mod entity; +pub mod instance; pub mod inventory; +pub mod math; mod packet; pub mod player_list; pub mod player_textures; pub mod server; -mod slab; -mod slab_rc; -mod slab_versioned; -pub mod util; -pub mod world; +#[cfg(any(test, doctest))] +mod unit_test; +pub mod view; -/// Use `valence::prelude::*` to import the most commonly used items from the -/// library. pub mod prelude { + pub use async_trait::async_trait; + pub use bevy_app::App; + pub use bevy_ecs::prelude::*; pub use biome::{Biome, BiomeId}; - pub use chunk::{Chunk, ChunkPos, Chunks, LoadedChunk, UnloadedChunk}; - pub use client::{Client, ClientEvent, ClientId, Clients}; - pub use config::{Config, ConnectionMode, PlayerSampleEntry, ServerListPing}; + pub use client::Client; + pub use config::{ + AsyncCallbacks, ConnectionMode, PlayerSampleEntry, ServerListPing, ServerPlugin, + }; pub use dimension::{Dimension, DimensionId}; - pub use entity::{Entities, Entity, EntityEvent, EntityId, EntityKind, TrackedData}; - pub use inventory::{Inventories, Inventory, InventoryId}; - pub use player_list::{PlayerList, PlayerListEntry, PlayerListId, PlayerLists}; - pub use server::{NewClientData, Server, SharedServer, ShutdownResult}; - pub use util::{from_yaw_and_pitch, to_yaw_and_pitch}; + pub use entity::{ + EntityAnimation, EntityKind, EntityStatus, McEntity, McEntityManager, TrackedData, + }; + pub use glam::DVec3; + pub use instance::{Chunk, Instance}; + pub use inventory::{Inventory, InventoryKind, OpenInventory}; + pub use player_list::{PlayerList, PlayerListEntry}; + pub use protocol::block::{BlockState, PropName, PropValue}; + pub use protocol::ident::Ident; + pub use protocol::text::{Color, Text, TextFormat}; + pub use protocol::types::GameMode; + pub use protocol::username::Username; + pub use protocol::{ident, ItemKind, ItemStack}; + pub use server::{EventLoop, NewClientInfo, Server, SharedServer}; pub use uuid::Uuid; pub use valence_nbt::Compound; - pub use valence_protocol::block::{PropName, PropValue}; - pub use valence_protocol::entity_meta::Pose; - pub use valence_protocol::ident::IdentError; - pub use valence_protocol::packets::s2c::particle::Particle; - pub use valence_protocol::packets::s2c::play::SetTitleAnimationTimes; - pub use valence_protocol::text::Color; - pub use valence_protocol::types::{GameMode, Hand, SoundCategory}; - pub use valence_protocol::{ - ident, translation_key, BlockKind, BlockPos, BlockState, Ident, InventoryKind, ItemKind, - ItemStack, Text, TextFormat, Username, MINECRAFT_VERSION, PROTOCOL_VERSION, - }; - pub use vek::{Aabb, Mat2, Mat3, Mat4, Vec2, Vec3, Vec4}; - pub use world::{World, WorldId, Worlds}; + pub use valence_protocol::{BlockKind, BlockPos}; + pub use view::{ChunkPos, ChunkView}; use super::*; - pub use crate::{async_trait, nbt, vek, Ticks, STANDARD_TPS}; } -/// The namespace for this library used internally for -/// [identifiers](valence_protocol::ident::Ident). +/// A [`Component`] for marking entities that should be despawned at the end of +/// the tick. +/// +/// In Valence, some built-in components such as [`McEntity`] are not allowed to +/// be removed from the [`World`] directly. Instead, you must give the entities +/// you wish to despawn the `Despawned` component. At the end of the tick, +/// Valence will despawn all entities with this component for you. +/// +/// It is legal to remove components or delete entities that Valence does not +/// know about at any time. +/// +/// [`McEntity`]: entity::McEntity +#[derive(Copy, Clone, Component)] +pub struct Despawned; + const LIBRARY_NAMESPACE: &str = "valence"; -/// The most recent version of the [Velocity] proxy which has been tested to -/// work with Valence. The elements of the tuple are (major, minor, patch) -/// version numbers. -/// -/// See [`Config::connection_mode`] to configure the proxy used with Valence. -/// -/// [Velocity]: https://velocitypowered.com/ -/// [`Config::connection_mode`]: config::Config::connection_mode -pub const SUPPORTED_VELOCITY_VERSION: (u16, u16, u16) = (3, 1, 2); - -/// A discrete unit of time where 1 tick is the duration of a -/// single game update. -/// -/// The duration of a game update on a Valence server depends on the current -/// configuration. In some contexts, "ticks" refer to the configured tick rate -/// while others refer to Minecraft's [standard TPS](STANDARD_TPS). -pub type Ticks = i64; - -/// Minecraft's standard ticks per second (TPS). -pub const STANDARD_TPS: Ticks = 20; +/// Let's pretend that [`NULL_ENTITY`] was created by spawning an entity, +/// immediately despawning it, and then stealing its [`Entity`] ID. The user +/// doesn't need to know about this. +const NULL_ENTITY: Entity = Entity::from_bits(u64::MAX); diff --git a/crates/valence/src/math.rs b/crates/valence/src/math.rs new file mode 100644 index 0000000..0ffc54d --- /dev/null +++ b/crates/valence/src/math.rs @@ -0,0 +1,88 @@ +pub use glam::*; + +/// An axis-aligned bounding box. `min` is expected to be <= `max` +/// componentwise. +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Aabb { + pub min: DVec3, + pub max: DVec3, +} + +impl Aabb { + pub fn new(p0: impl Into, p1: impl Into) -> Self { + let p0 = p0.into(); + let p1 = p1.into(); + Self { + min: p0.min(p1), + max: p0.max(p1), + } + } + + pub(crate) fn from_bottom_size(bottom: impl Into, size: impl Into) -> Self { + let bottom = bottom.into(); + let size = size.into(); + + Self { + min: DVec3 { + x: bottom.x - size.x / 2.0, + y: bottom.y, + z: bottom.z - size.z / 2.0, + }, + max: DVec3 { + x: bottom.x + size.x / 2.0, + y: bottom.y + size.y, + z: bottom.z + size.z / 2.0, + }, + } + } +} + +/// Takes a normalized direction vector and returns a `(yaw, pitch)` tuple in +/// degrees. +/// +/// This function is the inverse of [`from_yaw_and_pitch`] except for the case +/// where the direction is straight up or down. +#[track_caller] +pub fn to_yaw_and_pitch(d: Vec3) -> (f32, f32) { + debug_assert!(d.is_normalized(), "the given vector should be normalized"); + + let yaw = f32::atan2(d.z, d.x).to_degrees() - 90.0; + let pitch = -(d.y).asin().to_degrees(); + (yaw, pitch) +} + +/// Takes yaw and pitch angles (in degrees) and returns a normalized +/// direction vector. +/// +/// This function is the inverse of [`to_yaw_and_pitch`]. +pub fn from_yaw_and_pitch(yaw: f32, pitch: f32) -> Vec3 { + let (yaw_sin, yaw_cos) = (yaw + 90.0).to_radians().sin_cos(); + let (pitch_sin, pitch_cos) = (-pitch).to_radians().sin_cos(); + + Vec3::new(yaw_cos * pitch_cos, pitch_sin, yaw_sin * pitch_cos) +} + +/// Returns the minimum number of bits needed to represent the integer `n`. +pub(crate) const fn bit_width(n: usize) -> usize { + (usize::BITS - n.leading_zeros()) as _ +} + +#[cfg(test)] +mod tests { + use approx::assert_relative_eq; + use rand::random; + + use super::*; + + #[test] + fn yaw_pitch_round_trip() { + for _ in 0..=100 { + let d = (Vec3::new(random(), random(), random()) * 2.0 - 1.0).normalize(); + + let (yaw, pitch) = to_yaw_and_pitch(d); + let d_new = from_yaw_and_pitch(yaw, pitch); + + assert_relative_eq!(d, d_new, epsilon = f32::EPSILON * 100.0); + } + } +} diff --git a/crates/valence/src/packet.rs b/crates/valence/src/packet.rs index 4ab5c29..15aa450 100644 --- a/crates/valence/src/packet.rs +++ b/crates/valence/src/packet.rs @@ -1,29 +1,30 @@ use std::io::Write; -use valence_protocol::{encode_packet, encode_packet_compressed, EncodePacket}; +use tracing::warn; +use valence_protocol::{encode_packet, encode_packet_compressed, EncodePacket, PacketEncoder}; -pub trait WritePacket { - fn write_packet

(&mut self, packet: &P) -> anyhow::Result<()> +pub(crate) trait WritePacket { + fn write_packet

(&mut self, packet: &P) where P: EncodePacket + ?Sized; - fn write_bytes(&mut self, bytes: &[u8]) -> anyhow::Result<()>; + fn write_packet_bytes(&mut self, bytes: &[u8]); } impl WritePacket for &mut W { - fn write_packet

(&mut self, packet: &P) -> anyhow::Result<()> + fn write_packet

(&mut self, packet: &P) where P: EncodePacket + ?Sized, { (*self).write_packet(packet) } - fn write_bytes(&mut self, bytes: &[u8]) -> anyhow::Result<()> { - (*self).write_bytes(bytes) + fn write_packet_bytes(&mut self, bytes: &[u8]) { + (*self).write_packet_bytes(bytes) } } -pub struct PacketWriter<'a> { +pub(crate) struct PacketWriter<'a> { buf: &'a mut Vec, threshold: Option, scratch: &'a mut Vec, @@ -40,18 +41,39 @@ impl<'a> PacketWriter<'a> { } impl WritePacket for PacketWriter<'_> { - fn write_packet

(&mut self, pkt: &P) -> anyhow::Result<()> + fn write_packet

(&mut self, pkt: &P) where P: EncodePacket + ?Sized, { - if let Some(threshold) = self.threshold { + let res = if let Some(threshold) = self.threshold { encode_packet_compressed(self.buf, pkt, threshold, self.scratch) } else { encode_packet(self.buf, pkt) + }; + + if let Err(e) = res { + warn!("failed to write packet: {e:#}"); } } - fn write_bytes(&mut self, bytes: &[u8]) -> anyhow::Result<()> { - Ok(self.buf.write_all(bytes)?) + fn write_packet_bytes(&mut self, bytes: &[u8]) { + if let Err(e) = self.buf.write_all(bytes) { + warn!("failed to write packet bytes: {e:#}"); + } + } +} + +impl WritePacket for PacketEncoder { + fn write_packet

(&mut self, packet: &P) + where + P: EncodePacket + ?Sized, + { + if let Err(e) = self.append_packet(packet) { + warn!("failed to write packet: {e:#}"); + } + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.append_bytes(bytes) } } diff --git a/crates/valence/src/player_list.rs b/crates/valence/src/player_list.rs index 8ba715b..acd174f 100644 --- a/crates/valence/src/player_list.rs +++ b/crates/valence/src/player_list.rs @@ -1,414 +1,222 @@ -//! The player list (tab list). - -use std::collections::hash_map::Entry; -use std::collections::{HashMap, HashSet}; -use std::ops::{Deref, DerefMut, Index, IndexMut}; +use std::borrow::Cow; +use std::collections::hash_map::{Entry as MapEntry, OccupiedEntry as OccupiedMapEntry}; +use std::collections::HashMap; +use std::iter::FusedIterator; +use std::mem; +use bevy_ecs::prelude::*; +use tracing::warn; use uuid::Uuid; use valence_protocol::packets::s2c::play::{PlayerInfoRemove, SetTabListHeaderAndFooter}; use valence_protocol::packets::s2c::player_info_update::{ - Actions, Entry as PacketEntry, PlayerInfoUpdate, + Actions, Entry as PlayerInfoEntry, PlayerInfoUpdate, }; -use valence_protocol::types::{GameMode, SignedProperty}; +use valence_protocol::types::{GameMode, Property}; use valence_protocol::Text; -use crate::config::Config; +use crate::client::Client; use crate::packet::{PacketWriter, WritePacket}; -use crate::player_textures::SignedPlayerTextures; -use crate::slab_rc::{Key, RcSlab}; +use crate::server::Server; -/// A container for all [`PlayerList`]s on a server. -pub struct PlayerLists { - slab: RcSlab>, -} - -/// An identifier for a [`PlayerList`] on the server. -/// -/// Player list IDs are refcounted. Once all IDs referring to the same player -/// list are dropped, the player list is automatically deleted. -/// -/// The [`Ord`] instance on this type is correct but otherwise unspecified. This -/// is useful for storing IDs in containers such as -/// [`BTreeMap`](std::collections::BTreeMap). -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct PlayerListId(Key); - -impl PlayerLists { - pub(crate) fn new() -> Self { - Self { - slab: RcSlab::new(), - } - } - - /// Creates a new player list and returns an exclusive reference to it along - /// with its ID. - /// - /// The player list is automatically removed at the end of the tick once all - /// IDs to it have been dropped. - pub fn insert(&mut self, state: C::PlayerListState) -> (PlayerListId, &mut PlayerList) { - let (key, pl) = self.slab.insert(PlayerList { - state, - cached_update_packets: vec![], - entries: HashMap::new(), - removed: HashSet::new(), - header: Text::default(), - footer: Text::default(), - modified_header_or_footer: false, - }); - - (PlayerListId(key), pl) - } - - /// Gets a shared reference to the player list with the given player list - /// ID. - /// - /// This operation is infallible because [`PlayerListId`] is refcounted. - pub fn get(&self, id: &PlayerListId) -> &PlayerList { - self.slab.get(&id.0) - } - - /// Gets an exclusive reference to the player list with the given player - /// list ID. - /// - /// This operation is infallible because [`PlayerListId`] is refcounted. - pub fn get_mut(&mut self, id: &PlayerListId) -> &mut PlayerList { - self.slab.get_mut(&id.0) - } - - pub(crate) fn update_caches(&mut self, compression_threshold: Option) { - let mut scratch = vec![]; - - // Cache the update packets for each player list. - for pl in self.slab.iter_mut() { - pl.cached_update_packets.clear(); - - let mut writer = PacketWriter::new( - &mut pl.cached_update_packets, - compression_threshold, - &mut scratch, - ); - - if !pl.removed.is_empty() { - writer - .write_packet(&PlayerInfoRemove { - players: pl.removed.iter().cloned().collect(), - }) - .unwrap(); - } - - for (&uuid, entry) in pl.entries.iter_mut() { - if entry.created_this_tick { - // Send packets to initialize this entry. - - let mut actions = Actions::new().with_add_player(true); - - // We don't need to send data for fields if they have the default values. - - if entry.listed { - actions.set_update_listed(true); - } - - // Negative pings indicate absence. - if entry.ping >= 0 { - actions.set_update_latency(true); - } - - if entry.game_mode != GameMode::default() { - actions.set_update_game_mode(true); - } - - if entry.display_name.is_some() { - actions.set_update_display_name(true); - } - - // Don't forget to clear modified flags. - entry.old_listed = entry.listed; - entry.modified_ping = false; - entry.modified_game_mode = false; - entry.modified_display_name = false; - entry.created_this_tick = false; - - let entries = vec![PacketEntry { - player_uuid: uuid, - username: &entry.username, - properties: entry - .textures - .as_ref() - .map(|textures| SignedProperty { - name: "textures", - value: textures.payload(), - signature: Some(textures.signature()), - }) - .into_iter() - .collect(), - chat_data: None, - listed: entry.listed, - ping: entry.ping, - game_mode: entry.game_mode, - display_name: entry.display_name.clone(), - }]; - - writer - .write_packet(&PlayerInfoUpdate { actions, entries }) - .unwrap(); - } else { - let mut actions = Actions::new(); - - if entry.modified_ping { - entry.modified_ping = false; - actions.set_update_latency(true); - } - - if entry.modified_game_mode { - entry.modified_game_mode = false; - actions.set_update_game_mode(true); - } - - if entry.old_listed != entry.listed { - entry.old_listed = entry.listed; - actions.set_update_listed(true); - } - - if entry.modified_ping { - entry.modified_ping = false; - actions.set_update_latency(true); - } - - if entry.modified_display_name { - entry.modified_display_name = false; - actions.set_update_display_name(true); - } - - if u8::from(actions) != 0 { - writer - .write_packet(&PlayerInfoUpdate { - actions, - entries: vec![PacketEntry { - player_uuid: uuid, - username: &entry.username, - properties: vec![], - chat_data: None, - listed: entry.listed, - ping: entry.ping, - game_mode: entry.game_mode, - display_name: entry.display_name.clone(), - }], - }) - .unwrap(); - } - } - } - } - } - - pub(crate) fn clear_removed(&mut self) { - for pl in self.slab.iter_mut() { - pl.removed.clear(); - } - } -} - -impl<'a, C: Config> Index<&'a PlayerListId> for PlayerLists { - type Output = PlayerList; - - fn index(&self, index: &'a PlayerListId) -> &Self::Output { - self.get(index) - } -} - -impl<'a, C: Config> IndexMut<&'a PlayerListId> for PlayerLists { - fn index_mut(&mut self, index: &'a PlayerListId) -> &mut Self::Output { - self.get_mut(index) - } -} - -/// The list of players on a server visible by pressing the tab key by default. +/// The global list of players on a server visible by pressing the tab key by +/// default. /// /// Each entry in the player list is intended to represent a connected client to -/// the server. +/// the server. In addition to a list of players, the player list has a header +/// and a footer which can contain arbitrary text. /// -/// In addition to a list of players, the player list has a header and a footer -/// which can contain arbitrary text. -pub struct PlayerList { - /// Custom state - pub state: C::PlayerListState, +/// ```ignore +/// # use uuid::Uuid; +/// # use valence::player_list::{PlayerList, PlayerListEntry}; +/// +/// # let mut player_list = PlayerList::new(); +/// player_list.set_header("Hello, world!"); +/// player_list.set_footer("Goodbye, world!"); +/// player_list.insert( +/// Uuid::new_v4(), +/// PlayerListEntry::new() +/// .with_username("Notch") +/// .with_display_name(Some("Herobrine")), +/// ); +/// ``` +#[derive(Debug, Resource)] +pub struct PlayerList { cached_update_packets: Vec, - entries: HashMap, - /// Contains entries that need to be removed. - removed: HashSet, + entries: HashMap>, header: Text, footer: Text, modified_header_or_footer: bool, } -impl Deref for PlayerList { - type Target = C::PlayerListState; +impl PlayerList { + /// Returns a set of systems for maintaining the player list in a reasonable + /// default way. When clients connect, they are added to the player list. + /// When clients disconnect, they are removed from the player list. + pub fn default_system_set() -> SystemSet { + fn add_new_clients_to_player_list( + clients: Query<&Client, Added>, + mut player_list: ResMut, + ) { + for client in &clients { + let entry = PlayerListEntry::new() + .with_username(client.username()) + .with_properties(client.properties()) + .with_game_mode(client.game_mode()) + .with_ping(-1); // TODO - fn deref(&self) -> &Self::Target { - &self.state - } -} + player_list.insert(client.uuid(), entry); + } + } -impl DerefMut for PlayerList { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.state - } -} - -impl PlayerList { - /// Inserts a player into the player list. - /// - /// If the given UUID conflicts with an existing entry, the entry is - /// overwritten and `false` is returned. Otherwise, `true` is returned. - #[allow(clippy::too_many_arguments)] - pub fn insert( - &mut self, - uuid: Uuid, - username: impl Into, - textures: Option, - game_mode: GameMode, - ping: i32, - display_name: Option, - listed: bool, - ) -> bool { - match self.entries.entry(uuid) { - Entry::Occupied(mut oe) => { - let e = oe.get_mut(); - let username = username.into(); - - if e.username() != username || e.textures != textures { - // Entries created this tick haven't been initialized by clients yet, so there - // is nothing to remove. - if !e.created_this_tick { - self.removed.insert(*oe.key()); - } - - oe.insert(PlayerListEntry { - username, - textures, - game_mode, - ping, - display_name, - old_listed: listed, - listed, - created_this_tick: true, - modified_game_mode: false, - modified_ping: false, - modified_display_name: false, - }); - } else { - e.set_game_mode(game_mode); - e.set_ping(ping); - e.set_display_name(display_name); - e.set_listed(listed); + fn remove_disconnected_clients_from_player_list( + clients: Query<&mut Client>, + mut player_list: ResMut, + ) { + for client in &clients { + if client.is_disconnected() { + player_list.remove(client.uuid()); } - - false } + } + + SystemSet::new() + .with_system(add_new_clients_to_player_list) + .with_system(remove_disconnected_clients_from_player_list) + } +} + +impl PlayerList { + /// Create a new empty player list. + pub(crate) fn new() -> Self { + Self { + cached_update_packets: vec![], + entries: HashMap::new(), + header: Text::default(), + footer: Text::default(), + modified_header_or_footer: false, + } + } + + /// Get the entry for the given UUID, if it exists, otherwise return None. + pub fn get(&self, uuid: Uuid) -> Option<&PlayerListEntry> { + self.entries.get(&uuid).and_then(|opt| opt.as_ref()) + } + + /// Mutably get the entry for the given UUID, if it exists, otherwise return + /// None. + pub fn get_mut(&mut self, uuid: Uuid) -> Option<&mut PlayerListEntry> { + self.entries.get_mut(&uuid).and_then(|opt| opt.as_mut()) + } + + /// Get an iterator over all entries in the player list. The order of this + /// iterator is not guaranteed. + pub fn iter(&self) -> impl FusedIterator + Clone + '_ { + self.entries + .iter() + .filter_map(|(&uuid, opt)| opt.as_ref().map(|entry| (uuid, entry))) + } + + /// Get an iterator over all entries in the player list as mutable. The + /// order of this iterator is not guaranteed. + pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { + self.entries + .iter_mut() + .filter_map(|(&uuid, opt)| opt.as_mut().map(|entry| (uuid, entry))) + } + + /// Insert a new entry into the player list. If an entry already exists for + /// the given UUID, it is replaced and returned. + pub fn insert(&mut self, uuid: Uuid, entry: PlayerListEntry) -> Option { + match self.entry(uuid) { + Entry::Occupied(mut oe) => Some(oe.insert(entry)), Entry::Vacant(ve) => { - ve.insert(PlayerListEntry { - username: username.into(), - textures, - game_mode, - ping, - display_name, - old_listed: listed, - listed, - created_this_tick: true, - modified_game_mode: false, - modified_ping: false, - modified_display_name: false, - }); - - true + ve.insert(entry); + None } } } - /// Removes an entry from the player list with the given UUID. Returns - /// whether the entry was present in the list. - pub fn remove(&mut self, uuid: Uuid) -> bool { - if self.entries.remove(&uuid).is_some() { - self.removed.insert(uuid); - true - } else { - false + /// Remove an entry from the player list. If an entry exists for the given + /// UUID, it is removed and returned. + pub fn remove(&mut self, uuid: Uuid) -> Option { + match self.entry(uuid) { + Entry::Occupied(oe) => Some(oe.remove()), + Entry::Vacant(_) => None, } } - /// Removes all entries from the player list for which `f` returns `false`. - /// - /// All entries are visited in an unspecified order. - pub fn retain(&mut self, mut f: impl FnMut(Uuid, &mut PlayerListEntry) -> bool) { - self.entries.retain(|&uuid, entry| { - if !f(uuid, entry) { - self.removed.insert(uuid); - false - } else { - true + /// Gets the given key’s corresponding entry in the map for in-place + /// manipulation. + pub fn entry(&mut self, uuid: Uuid) -> Entry { + match self.entries.entry(uuid) { + MapEntry::Occupied(oe) if oe.get().is_some() => { + Entry::Occupied(OccupiedEntry { entry: oe }) } - }) + MapEntry::Occupied(oe) => Entry::Vacant(VacantEntry { + entry: MapEntry::Occupied(oe), + }), + MapEntry::Vacant(ve) => Entry::Vacant(VacantEntry { + entry: MapEntry::Vacant(ve), + }), + } } - /// Removes all entries from the player list. - pub fn clear(&mut self) { - self.removed.extend(self.entries.drain().map(|p| p.0)) - } - - /// Gets the header part of the player list. pub fn header(&self) -> &Text { &self.header } - /// Sets the header part of the player list. - pub fn set_header(&mut self, header: impl Into) { + /// Set the header text for the player list. Returns the previous header. + pub fn set_header(&mut self, header: impl Into) -> Text { let header = header.into(); - if self.header != header { - self.header = header; + + if header != self.header { self.modified_header_or_footer = true; } + + mem::replace(&mut self.header, header) } - /// Gets the footer part of the player list. pub fn footer(&self) -> &Text { &self.footer } - /// Sets the footer part of the player list. - pub fn set_footer(&mut self, footer: impl Into) { + /// Set the footer text for the player list. Returns the previous footer. + pub fn set_footer(&mut self, footer: impl Into) -> Text { let footer = footer.into(); - if self.footer != footer { - self.footer = footer; + + if footer != self.footer { self.modified_header_or_footer = true; } + + mem::replace(&mut self.footer, footer) } - /// Returns a reference to the entry with the given UUID. + /// Retains only the elements specified by the predicate. /// - /// If the entry does not exist, `None` is returned. - pub fn entry(&self, uuid: Uuid) -> Option<&PlayerListEntry> { - self.entries.get(&uuid) + /// In other words, remove all pairs `(k, v)` for which `f(&k, &mut v)` + /// returns `false`. The elements are visited in unsorted (and + /// unspecified) order. + pub fn retain(&mut self, mut f: F) + where + F: FnMut(Uuid, &mut PlayerListEntry) -> bool, + { + self.entries.retain(|&uuid, opt| { + if let Some(entry) = opt { + if !f(uuid, entry) { + *opt = None; + } + } + + true + }); } - /// Returns a mutable reference to the entry with the given UUID. - /// - /// If the entry does not exist, `None` is returned. - pub fn entry_mut(&mut self, uuid: Uuid) -> Option<&mut PlayerListEntry> { - self.entries.get_mut(&uuid) + /// Clear the player list. + pub fn clear(&mut self) { + self.entries.values_mut().for_each(|e| *e = None); } - /// Returns an iterator over all entries in an unspecified order. - pub fn entries(&self) -> impl Iterator + '_ { - self.entries.iter().map(|(k, v)| (*k, v)) - } - - /// Returns a mutable iterator over all entries in an unspecified order. - pub fn entries_mut(&mut self) -> impl Iterator + '_ { - self.entries.iter_mut().map(|(k, v)| (*k, v)) - } - - /// Writes the packets needed to completely initialize this player list. - pub(crate) fn write_init_packets(&self, mut writer: impl WritePacket) -> anyhow::Result<()> { + pub(crate) fn write_init_packets(&self, mut writer: impl WritePacket) { let actions = Actions::new() .with_add_player(true) .with_update_game_mode(true) @@ -419,110 +227,190 @@ impl PlayerList { let entries: Vec<_> = self .entries .iter() - .map(|(&uuid, entry)| { - let properties = entry - .textures - .as_ref() - .map(|textures| SignedProperty { - name: "textures", - value: textures.payload(), - signature: Some(textures.signature()), - }) - .into_iter() - .collect(); - - PacketEntry { + .filter_map(|(&uuid, opt)| { + opt.as_ref().map(|entry| PlayerInfoEntry { player_uuid: uuid, - username: entry.username(), - properties, + username: &entry.username, + properties: entry.properties().into(), chat_data: None, listed: entry.listed, ping: entry.ping, game_mode: entry.game_mode, - display_name: entry.display_name.clone(), - } + display_name: entry.display_name.as_ref().map(|t| t.into()), + }) }) .collect(); if !entries.is_empty() { - writer.write_packet(&PlayerInfoUpdate { actions, entries })?; + writer.write_packet(&PlayerInfoUpdate { + actions, + entries: entries.into(), + }); } if !self.header.is_empty() || !self.footer.is_empty() { writer.write_packet(&SetTabListHeaderAndFooter { - header: self.header.clone(), - footer: self.footer.clone(), - })?; + header: (&self.header).into(), + footer: (&self.footer).into(), + }); } - - Ok(()) - } - - /// Writes the packet needed to update this player list from the previous - /// state to the current state. - pub(crate) fn write_update_packets(&self, mut writer: impl WritePacket) -> anyhow::Result<()> { - writer.write_bytes(&self.cached_update_packets) - } - - /// Writes all the packets needed to completely clear this player list. - pub(crate) fn write_clear_packets(&self, mut writer: impl WritePacket) -> anyhow::Result<()> { - let uuids = self - .entries - .keys() - .cloned() - .chain(self.removed.iter().cloned()) - .collect(); - - writer.write_packet(&PlayerInfoRemove { players: uuids }) } } /// Represents a player entry in the [`PlayerList`]. +/// +/// ``` +/// use valence::player_list::PlayerListEntry; +/// +/// PlayerListEntry::new() +/// .with_username("Notch") +/// .with_display_name(Some("Herobrine")); +/// ``` +#[derive(Clone, Debug)] pub struct PlayerListEntry { - username: String, - textures: Option, + username: String, // TODO: Username? + properties: Vec, game_mode: GameMode, + old_game_mode: GameMode, ping: i32, display_name: Option, - old_listed: bool, listed: bool, - created_this_tick: bool, - modified_game_mode: bool, + old_listed: bool, + is_new: bool, modified_ping: bool, modified_display_name: bool, } +impl Default for PlayerListEntry { + fn default() -> Self { + Self { + username: String::new(), + properties: vec![], + game_mode: GameMode::default(), + old_game_mode: GameMode::default(), + ping: -1, // Negative indicates absence. + display_name: None, + old_listed: true, + listed: true, + is_new: true, + modified_ping: false, + modified_display_name: false, + } + } +} + impl PlayerListEntry { - /// Gets the username of this entry. + /// Create a new player list entry. + /// + /// ``` + /// use valence::player_list::PlayerListEntry; + /// + /// PlayerListEntry::new() + /// .with_username("Notch") + /// .with_display_name(Some("Herobrine")); + /// ``` + pub fn new() -> Self { + Self::default() + } + + /// Set the username for the player list entry. Returns `Self` to chain + /// other options. + #[must_use] + pub fn with_username(mut self, username: impl Into) -> Self { + self.username = username.into(); + + if self.username.chars().count() > 16 { + warn!("player list username is longer than 16 characters"); + } + + self + } + + /// Set the properties for the player list entry. Returns `Self` to chain + /// other options. + /// + /// A property is a key-value pair that can be used to customize the + /// appearance of the player list entry. For example, the skin of the + /// player can be set by adding a property with the key `textures` and + /// the value being a base64 encoded JSON object. + #[must_use] + pub fn with_properties(mut self, properties: impl Into>) -> Self { + self.properties = properties.into(); + self + } + + /// Set the game mode for the player list entry. Returns `Self` to chain + /// other options. + #[must_use] + pub fn with_game_mode(mut self, game_mode: GameMode) -> Self { + self.game_mode = game_mode; + self + } + + /// Set the ping for the player list entry. Returns `Self` to chain other + /// options. + /// + /// The ping is the number of milliseconds it takes for the server to + /// receive a response from the player. The client will display the + /// ping as a number of green bars, where more bars indicate a lower + /// ping. + /// + /// Use a value of `-1` to hide the ping. + #[must_use] + pub fn with_ping(mut self, ping: i32) -> Self { + self.ping = ping; + self + } + + /// Set the display name for the player list entry. Returns `Self` to + /// chain other options. + /// + /// The display name is the literal text that is displayed in the player + /// list. If this is not set, the username will be used instead. + #[must_use] + pub fn with_display_name(mut self, display_name: Option>) -> Self { + self.display_name = display_name.map(Into::into); + self + } + + /// Set whether the player list entry is listed. Returns `Self` to chain + /// other options. Setting this to `false` will hide the entry from the + /// player list. + #[must_use] + pub fn with_listed(mut self, listed: bool) -> Self { + self.listed = listed; + self + } + pub fn username(&self) -> &str { &self.username } - /// Gets the player textures for this entry. - pub fn textures(&self) -> Option<&SignedPlayerTextures> { - self.textures.as_ref() + pub fn properties(&self) -> &[Property] { + &self.properties } - /// Gets the game mode of this entry. pub fn game_mode(&self) -> GameMode { self.game_mode } - /// Sets the game mode of this entry. + /// Set the game mode for the player list entry. pub fn set_game_mode(&mut self, game_mode: GameMode) { - if self.game_mode != game_mode { - self.game_mode = game_mode; - // TODO: replace modified_game_mode with old_game_mode - self.modified_game_mode = true; - } + self.game_mode = game_mode; } - /// Gets the ping (latency) of this entry measured in milliseconds. pub fn ping(&self) -> i32 { self.ping } - /// Sets the ping (latency) of this entry measured in milliseconds. + /// Set the ping for the player list entry. + /// + /// The ping is the number of milliseconds it takes for the server to + /// receive a response from the player. The client will display the + /// ping as a number of green bars, where more bars indicate a lower + /// ping. + /// + /// Use a value of `-1` to hide the ping. pub fn set_ping(&mut self, ping: i32) { if self.ping != ping { self.ping = ping; @@ -530,27 +418,265 @@ impl PlayerListEntry { } } - /// Gets the display name of this entry. pub fn display_name(&self) -> Option<&Text> { self.display_name.as_ref() } - /// Sets the display name of this entry. - pub fn set_display_name(&mut self, display_name: impl Into>) { - let display_name = display_name.into(); + /// Set the display name for the player list entry. Returns the previous + /// display name, if any. + /// + /// The display name is the literal text that is displayed in the player + /// list. If this is not set, the username will be used instead. + pub fn set_display_name(&mut self, display_name: Option>) -> Option { + let display_name = display_name.map(Into::into); + if self.display_name != display_name { - self.display_name = display_name; self.modified_display_name = true; } + + mem::replace(&mut self.display_name, display_name) } - /// If this entry is visible on the player list. pub fn is_listed(&self) -> bool { self.listed } - /// Sets if this entry is visible on the player list. + /// Set whether the player list entry is listed. Setting this to `false` + /// will hide the entry from the player list. pub fn set_listed(&mut self, listed: bool) { self.listed = listed; } + + fn clear_trackers(&mut self) { + self.old_game_mode = self.game_mode; + self.old_listed = self.listed; + self.modified_ping = false; + self.modified_display_name = false; + } +} + +/// An entry in the player list that corresponds to a single UUID. Works like +/// [`std::collections::hash_map::Entry`]. +#[derive(Debug)] +pub enum Entry<'a> { + Occupied(OccupiedEntry<'a>), + Vacant(VacantEntry<'a>), +} + +#[derive(Debug)] +pub struct OccupiedEntry<'a> { + entry: OccupiedMapEntry<'a, Uuid, Option>, +} + +impl<'a> OccupiedEntry<'a> { + pub fn key(&self) -> &Uuid { + self.entry.key() + } + + pub fn remove_entry(mut self) -> (Uuid, PlayerListEntry) { + let mut entry = self.entry.get_mut().take().unwrap(); + let uuid = *self.entry.key(); + + entry.is_new = false; + + (uuid, entry) + } + + pub fn get(&self) -> &PlayerListEntry { + self.entry.get().as_ref().unwrap() + } + + pub fn get_mut(&mut self) -> &mut PlayerListEntry { + self.entry.get_mut().as_mut().unwrap() + } + + pub fn into_mut(self) -> &'a mut PlayerListEntry { + self.entry.into_mut().as_mut().unwrap() + } + + pub fn insert(&mut self, mut entry: PlayerListEntry) -> PlayerListEntry { + let old_entry = self.get_mut(); + + // Need to overwrite the entry if the username or properties changed because the + // player list update packet doesn't support modifying these. Otherwise we can + // just modify the existing entry. + if old_entry.username != entry.username || old_entry.properties != entry.properties { + entry.clear_trackers(); + entry.is_new = true; + self.entry.insert(Some(entry)).unwrap() + } else { + PlayerListEntry::new() + .with_game_mode(old_entry.game_mode) + .with_ping(old_entry.ping) + .with_display_name(old_entry.set_display_name(entry.display_name)) + .with_listed(old_entry.listed) + } + } + + pub fn remove(self) -> PlayerListEntry { + self.remove_entry().1 + } +} + +#[derive(Debug)] +pub struct VacantEntry<'a> { + entry: MapEntry<'a, Uuid, Option>, +} + +impl<'a> VacantEntry<'a> { + pub fn key(&self) -> &Uuid { + self.entry.key() + } + + pub fn into_key(self) -> Uuid { + *self.entry.key() + } + + pub fn insert(self, mut entry: PlayerListEntry) -> &'a mut PlayerListEntry { + entry.clear_trackers(); + entry.is_new = true; + + match self.entry { + MapEntry::Occupied(mut oe) => { + oe.insert(Some(entry)); + oe.into_mut().as_mut().unwrap() + } + MapEntry::Vacant(ve) => ve.insert(Some(entry)).as_mut().unwrap(), + } + } +} + +/// Manage all player lists on the server and send updates to clients. +pub(crate) fn update_player_list( + player_list: ResMut, + server: Res, + mut clients: Query<&mut Client>, +) { + let pl = player_list.into_inner(); + + let mut scratch = vec![]; + pl.cached_update_packets.clear(); + + let mut writer = PacketWriter::new( + &mut pl.cached_update_packets, + server.compression_threshold(), + &mut scratch, + ); + + let mut removed = vec![]; + + pl.entries.retain(|&uuid, entry| { + let Some(entry) = entry else { + removed.push(uuid); + return false + }; + + if entry.is_new { + entry.is_new = false; + + // Send packets to initialize this entry. + + let mut actions = Actions::new().with_add_player(true); + + // We don't need to send data for fields if they have the default values. + + if entry.listed { + actions.set_update_listed(true); + } + + // Negative ping indicates absence. + if entry.ping != 0 { + actions.set_update_latency(true); + } + + if entry.game_mode != GameMode::default() { + actions.set_update_game_mode(true); + } + + if entry.display_name.is_some() { + actions.set_update_display_name(true); + } + + entry.clear_trackers(); + + let packet_entry = PlayerInfoEntry { + player_uuid: uuid, + username: &entry.username, + properties: Cow::Borrowed(&entry.properties), + chat_data: None, + listed: entry.listed, + ping: entry.ping, + game_mode: entry.game_mode, + display_name: entry.display_name.as_ref().map(|t| t.into()), + }; + + writer.write_packet(&PlayerInfoUpdate { + actions, + entries: Cow::Borrowed(&[packet_entry]), + }); + } else { + let mut actions = Actions::new(); + + if entry.game_mode != entry.old_game_mode { + entry.old_game_mode = entry.game_mode; + actions.set_update_game_mode(true); + } + + if entry.listed != entry.old_listed { + entry.old_listed = entry.listed; + actions.set_update_listed(true); + } + + if entry.modified_ping { + entry.modified_ping = false; + actions.set_update_latency(true); + } + + if entry.modified_display_name { + entry.modified_display_name = false; + actions.set_update_display_name(true); + } + + if u8::from(actions) != 0 { + writer.write_packet(&PlayerInfoUpdate { + actions, + entries: Cow::Borrowed(&[PlayerInfoEntry { + player_uuid: uuid, + username: &entry.username, + properties: Cow::default(), + chat_data: None, + listed: entry.listed, + ping: entry.ping, + game_mode: entry.game_mode, + display_name: entry.display_name.as_ref().map(|t| t.into()), + }]), + }); + } + } + + true + }); + + if !removed.is_empty() { + writer.write_packet(&PlayerInfoRemove { + uuids: removed.into(), + }); + } + + if pl.modified_header_or_footer { + pl.modified_header_or_footer = false; + + writer.write_packet(&SetTabListHeaderAndFooter { + header: (&pl.header).into(), + footer: (&pl.footer).into(), + }); + } + + for mut client in &mut clients { + if client.is_new() { + pl.write_init_packets(client.into_inner()); + } else { + client.write_packet_bytes(&pl.cached_update_packets); + } + } } diff --git a/crates/valence/src/player_textures.rs b/crates/valence/src/player_textures.rs index af043c1..2c806df 100644 --- a/crates/valence/src/player_textures.rs +++ b/crates/valence/src/player_textures.rs @@ -1,35 +1,27 @@ //! Player skins and capes. +use anyhow::Context; +use base64::prelude::*; use serde::Deserialize; use url::Url; +use valence_protocol::types::Property; /// Contains URLs to the skin and cape of a player. -/// -/// This data has been cryptographically signed to ensure it will not be altered -/// by the server. #[derive(Clone, PartialEq, Eq, Debug)] -pub struct SignedPlayerTextures { - payload: Box, - signature: Box, - skin_url: Box, - cape_url: Option>, +pub struct PlayerTextures { + /// URL to the player's skin texture. + pub skin: Url, + /// URL to the player's cape texture. May be absent if the player does not + /// have a cape. + pub cape: Option, } -impl SignedPlayerTextures { - /// Constructs the signed player textures from payload and signature - /// components in base64. - /// - /// Note that this does not validate that the signature is valid for the - /// given payload. - pub(crate) fn from_base64( - payload: impl Into>, - signature: impl Into>, - ) -> anyhow::Result { - let payload = payload.into(); - let signature = signature.into(); - - let payload_decoded = base64::decode(payload.as_bytes())?; - base64::decode(signature.as_bytes())?; +impl PlayerTextures { + pub fn from_properties(props: &[Property]) -> anyhow::Result { + let textures = props + .iter() + .find(|p| p.name == "textures") + .context("no textures in property list")?; #[derive(Debug, Deserialize)] struct Textures { @@ -49,38 +41,13 @@ impl SignedPlayerTextures { url: Url, } - let textures: Textures = serde_json::from_slice(&payload_decoded)?; + let decoded = BASE64_STANDARD.decode(textures.value.as_bytes())?; + + let Textures { textures } = serde_json::from_slice(&decoded)?; Ok(Self { - payload, - signature, - skin_url: String::from(textures.textures.skin.url).into(), - cape_url: textures.textures.cape.map(|t| String::from(t.url).into()), + skin: textures.skin.url, + cape: textures.cape.map(|t| t.url), }) } - - /// The payload in base64. - pub(crate) fn payload(&self) -> &str { - &self.payload - } - - /// The signature in base64. - pub(crate) fn signature(&self) -> &str { - &self.signature - } - - /// Returns the URL to the texture's skin as a `str`. - /// - /// The returned string is guaranteed to be a valid URL. - pub fn skin(&self) -> &str { - &self.skin_url - } - - /// Returns the URL to the texture's cape as a `str` if present. - /// - /// The returned string is guaranteed to be a valid URL. `None` is returned - /// instead if there is no cape. - pub fn cape(&self) -> Option<&str> { - self.cape_url.as_deref() - } } diff --git a/crates/valence/src/server.rs b/crates/valence/src/server.rs index 59d115b..3dd1113 100644 --- a/crates/valence/src/server.rs +++ b/crates/valence/src/server.rs @@ -1,126 +1,87 @@ -//! The heart of the server. - -use std::error::Error; use std::iter::FusedIterator; use std::net::{IpAddr, SocketAddr}; -use std::ops::{Deref, DerefMut}; +use std::ops::Deref; use std::sync::{Arc, Mutex}; +use std::thread; use std::time::{Duration, Instant}; -use std::{io, thread}; -use anyhow::{ensure, Context}; +use anyhow::ensure; +use bevy_app::prelude::*; +use bevy_app::AppExit; +use bevy_ecs::event::ManualEventReader; +use bevy_ecs::prelude::*; use flume::{Receiver, Sender}; -pub(crate) use packet_manager::{PlayPacketReceiver, PlayPacketSender}; use rand::rngs::OsRng; -use rayon::iter::ParallelIterator; -use reqwest::Client as ReqwestClient; use rsa::{PublicKeyParts, RsaPrivateKey}; -use serde_json::{json, Value}; -use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; -use tokio::net::{TcpListener, TcpStream}; use tokio::runtime::{Handle, Runtime}; -use tokio::sync::{OwnedSemaphorePermit, Semaphore}; -use tracing::{error, info, info_span, instrument, trace, warn}; +use tokio::sync::Semaphore; use uuid::Uuid; use valence_nbt::{compound, Compound, List}; -use valence_protocol::packets::c2s::handshake::HandshakeOwned; -use valence_protocol::packets::c2s::login::LoginStart; -use valence_protocol::packets::c2s::status::{PingRequest, StatusRequest}; -use valence_protocol::packets::s2c::login::{DisconnectLogin, LoginSuccess, SetCompression}; -use valence_protocol::packets::s2c::status::{PingResponse, StatusResponse}; -use valence_protocol::types::HandshakeNextState; -use valence_protocol::{ - ident, PacketDecoder, PacketEncoder, Username, VarInt, MINECRAFT_VERSION, PROTOCOL_VERSION, -}; +use valence_protocol::types::Property; +use valence_protocol::{ident, Username}; use crate::biome::{validate_biomes, Biome, BiomeId}; -use crate::chunk::entity_partition::update_entity_partition; -use crate::client::{Client, Clients}; -use crate::config::{Config, ConnectionMode, ServerListPing}; +use crate::client::event::{event_loop_run_criteria, register_client_events}; +use crate::client::{update_clients, Client}; +use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin}; use crate::dimension::{validate_dimensions, Dimension, DimensionId}; -use crate::entity::Entities; -use crate::inventory::Inventories; -use crate::player_list::PlayerLists; -use crate::player_textures::SignedPlayerTextures; -use crate::server::packet_manager::InitialPacketManager; -use crate::world::Worlds; -use crate::Ticks; +use crate::entity::{ + check_entity_invariants, deinit_despawned_entities, init_entities, update_entities, + McEntityManager, +}; +use crate::instance::{ + check_instance_invariants, update_instances_post_client, update_instances_pre_client, Instance, +}; +use crate::inventory::{ + handle_click_container, handle_close_container, handle_set_held_item, handle_set_slot_creative, + update_client_on_close_inventory, update_open_inventories, update_player_inventories, + Inventory, InventoryKind, +}; +use crate::player_list::{update_player_list, PlayerList}; +use crate::server::connect::do_accept_loop; +use crate::Despawned; mod byte_channel; -mod login; -mod packet_manager; +mod connect; +pub(crate) mod connection; -/// Contains the entire state of a running Minecraft server, accessible from -/// within the [init] and [update] functions. -/// -/// [init]: crate::config::Config::init -/// [update]: crate::config::Config::update -pub struct Server { - /// Custom state. - pub state: C::ServerState, - /// A handle to this server's [`SharedServer`]. - pub shared: SharedServer, - /// All of the clients on the server. - pub clients: Clients, - /// All of entities on the server. - pub entities: Entities, - /// All of the worlds on the server. - pub worlds: Worlds, - /// All of the player lists on the server. - pub player_lists: PlayerLists, - /// All of the inventories on the server. - pub inventories: Inventories, - /// Incremented on every game tick. - current_tick: Ticks, - last_tick_duration: Duration, +/// Contains global server state accessible as a [`Resource`]. +#[derive(Resource)] +pub struct Server { + /// Incremented on every tick. + current_tick: i64, + shared: SharedServer, } -impl Server { - /// Returns the number of ticks that have elapsed since the server began. - pub fn current_tick(&self) -> Ticks { - self.current_tick - } - - /// Returns the amount of time taken to execute the previous tick, not - /// including the time spent sleeping. - pub fn last_tick_duration(&mut self) -> Duration { - self.last_tick_duration - } -} - -impl Deref for Server { - type Target = C::ServerState; +impl Deref for Server { + type Target = SharedServer; fn deref(&self) -> &Self::Target { - &self.state + &self.shared } } -impl DerefMut for Server { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.state +impl Server { + /// Provides a reference to the [`SharedServer`]. + pub fn shared(&self) -> &SharedServer { + &self.shared + } + + /// Returns the number of ticks that have elapsed since the server began. + pub fn current_tick(&self) -> i64 { + self.current_tick } } -/// A handle to a Minecraft server containing the subset of functionality which -/// is accessible outside the [update] loop. +/// The subset of global server state which can be shared between threads. /// -/// `SharedServer`s are internally refcounted and can -/// be shared between threads. -/// -/// [update]: crate::config::Config::update -pub struct SharedServer(Arc>); +/// `SharedServer`s are internally refcounted and are inexpensive to clone. +#[derive(Clone)] +pub struct SharedServer(Arc); -impl Clone for SharedServer { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -struct SharedServerInner { - cfg: C, +struct SharedServerInner { address: SocketAddr, - tick_rate: Ticks, + tps: i64, connection_mode: ConnectionMode, compression_threshold: Option, max_connections: usize, @@ -131,58 +92,36 @@ struct SharedServerInner { /// Holding a runtime handle is not enough to keep tokio working. We need /// to store the runtime here so we don't drop it. _tokio_runtime: Option, - dimensions: Vec, - biomes: Vec, + dimensions: Arc<[Dimension]>, + biomes: Arc<[Biome]>, /// Contains info about dimensions, biomes, and chats. /// Sent to all clients when joining. registry_codec: Compound, /// The instant the server was started. start_instant: Instant, + /// Sender for new clients past the login stage. + new_clients_send: Sender, /// Receiver for new clients past the login stage. - new_clients_send: Sender, - new_clients_recv: Receiver, + new_clients_recv: Receiver, /// A semaphore used to limit the number of simultaneous connections to the /// server. Closing this semaphore stops new connections. connection_sema: Arc, /// The result that will be returned when the server is shut down. - shutdown_result: Mutex>, + shutdown_result: Mutex>>, /// The RSA keypair used for encryption with clients. rsa_key: RsaPrivateKey, /// The public part of `rsa_key` encoded in DER, which is an ASN.1 format. /// This is sent to clients during the authentication process. public_key_der: Box<[u8]>, /// For session server requests. - http_client: ReqwestClient, + http_client: reqwest::Client, } -/// Contains information about a new client joining the server. -#[non_exhaustive] -pub struct NewClientData { - /// The username of the new client. - pub username: Username, - /// The UUID of the new client. - pub uuid: Uuid, - /// The remote address of the new client. - pub ip: IpAddr, - /// The new client's player textures. May be `None` if the client does not - /// have a skin or cape. - pub textures: Option, -} - -struct NewClientMessage { - ncd: NewClientData, - send: PlayPacketSender, - recv: PlayPacketReceiver, - permit: OwnedSemaphorePermit, -} - -/// The result type returned from [`start_server`]. -pub type ShutdownResult = Result<(), Box>; - -impl SharedServer { - /// Gets a reference to the config object used to start the server. - pub fn config(&self) -> &C { - &self.0.cfg +impl SharedServer { + /// Creates a new [`Instance`] component with the given dimension. + #[must_use] + pub fn new_instance(&self, dimension: DimensionId) -> Instance { + Instance::new(dimension, self) } /// Gets the socket address this server is bound to. @@ -190,9 +129,9 @@ impl SharedServer { self.0.address } - /// Gets the configured tick rate of this server. - pub fn tick_rate(&self) -> Ticks { - self.0.tick_rate + /// Gets the configured ticks per second of this server. + pub fn tps(&self) -> i64 { + self.0.tps } /// Gets the connection mode of the server. @@ -227,10 +166,6 @@ impl SharedServer { } /// Obtains a [`Dimension`] by using its corresponding [`DimensionId`]. - /// - /// It is safe but unspecified behavior to call this function using a - /// [`DimensionId`] not originating from the configuration used to construct - /// the server. pub fn dimension(&self, id: DimensionId) -> &Dimension { self.0 .dimensions @@ -249,10 +184,6 @@ impl SharedServer { } /// Obtains a [`Biome`] by using its corresponding [`BiomeId`]. - /// - /// It is safe but unspecified behavior to call this function using a - /// [`BiomeId`] not originating from the configuration used to construct - /// the server. pub fn biome(&self, id: BiomeId) -> &Biome { self.0.biomes.get(id.0 as usize).expect("invalid biome ID") } @@ -280,82 +211,49 @@ impl SharedServer { } /// Immediately stops new connections to the server and initiates server - /// shutdown. The given result is returned through [`start_server`]. + /// shutdown. /// /// You may want to disconnect all players with a message prior to calling /// this function. pub fn shutdown(&self, res: Result<(), E>) where - E: Into>, + E: Into, { self.0.connection_sema.close(); *self.0.shutdown_result.lock().unwrap() = Some(res.map_err(|e| e.into())); } } -/// Consumes the configuration and starts the server. -/// -/// This function blocks the current thread and returns once the server has shut -/// down, a runtime error occurs, or the configuration is found to be invalid. -pub fn start_server(config: C, data: C::ServerState) -> ShutdownResult { - let shared = setup_server(config) - .context("failed to initialize server") - .map_err(Box::::from)?; - - let _guard = shared.tokio_handle().enter(); - - let mut server = Server { - state: data, - shared: shared.clone(), - clients: Clients::new(), - entities: Entities::new(), - worlds: Worlds::new(shared.clone()), - player_lists: PlayerLists::new(), - inventories: Inventories::new(), - current_tick: 0, - last_tick_duration: Default::default(), - }; - - info_span!("configured_init").in_scope(|| shared.config().init(&mut server)); - - tokio::spawn(do_accept_loop(shared)); - - do_update_loop(&mut server) +/// Contains information about a new client joining the server. +#[non_exhaustive] +pub struct NewClientInfo { + /// The username of the new client. + pub username: Username, + /// The UUID of the new client. + pub uuid: Uuid, + /// The remote address of the new client. + pub ip: IpAddr, + /// The client's properties from the game profile. Typically contains a + /// `textures` property with the skin and cape of the player. + pub properties: Vec, } -#[instrument(skip_all)] -fn setup_server(cfg: C) -> anyhow::Result> { - let max_connections = cfg.max_connections(); - let address = cfg.address(); - let tick_rate = cfg.tick_rate(); - - ensure!(tick_rate > 0, "tick rate must be greater than zero"); - - let connection_mode = cfg.connection_mode(); - - let incoming_packet_capacity = cfg.incoming_capacity(); - +pub fn build_plugin( + plugin: &ServerPlugin, + app: &mut App, +) -> anyhow::Result<()> { ensure!( - incoming_packet_capacity > 0, - "serverbound packet capacity must be nonzero" + plugin.tps > 0, + "configured tick rate must be greater than zero" ); - - let outgoing_packet_capacity = cfg.outgoing_capacity(); - ensure!( - outgoing_packet_capacity > 0, - "outgoing packet capacity must be nonzero" + plugin.incoming_capacity > 0, + "configured incoming packet capacity must be nonzero" + ); + ensure!( + plugin.outgoing_capacity > 0, + "configured outgoing packet capacity must be nonzero" ); - - let compression_threshold = cfg.compression_threshold(); - - let tokio_handle = cfg.tokio_handle(); - - let dimensions = cfg.dimensions(); - validate_dimensions(&dimensions)?; - - let biomes = cfg.biomes(); - validate_biomes(&biomes)?; let rsa_key = RsaPrivateKey::new(&mut OsRng, 1024)?; @@ -363,9 +261,7 @@ fn setup_server(cfg: C) -> anyhow::Result> { rsa_der::public_key_to_der(&rsa_key.n().to_bytes_be(), &rsa_key.e().to_bytes_be()) .into_boxed_slice(); - let (new_clients_send, new_clients_recv) = flume::bounded(64); - - let runtime = if tokio_handle.is_none() { + let runtime = if plugin.tokio_handle.is_none() { Some(Runtime::new()?) } else { None @@ -373,56 +269,200 @@ fn setup_server(cfg: C) -> anyhow::Result> { let tokio_handle = match &runtime { Some(rt) => rt.handle().clone(), - None => tokio_handle.unwrap(), + None => plugin.tokio_handle.clone().unwrap(), }; - let registry_codec = make_registry_codec(&dimensions, &biomes); + validate_dimensions(&plugin.dimensions)?; + validate_biomes(&plugin.biomes)?; - let server = SharedServerInner { - cfg, - address, - tick_rate, - connection_mode, - compression_threshold, - max_connections, - incoming_capacity: incoming_packet_capacity, - outgoing_capacity: outgoing_packet_capacity, + let registry_codec = make_registry_codec(&plugin.dimensions, &plugin.biomes); + + let (new_clients_send, new_clients_recv) = flume::bounded(64); + + let shared = SharedServer(Arc::new(SharedServerInner { + address: plugin.address, + tps: plugin.tps, + connection_mode: plugin.connection_mode.clone(), + compression_threshold: plugin.compression_threshold, + max_connections: plugin.max_connections, + incoming_capacity: plugin.incoming_capacity, + outgoing_capacity: plugin.outgoing_capacity, tokio_handle, _tokio_runtime: runtime, - dimensions, - biomes, + dimensions: plugin.dimensions.clone(), + biomes: plugin.biomes.clone(), registry_codec, start_instant: Instant::now(), new_clients_send, new_clients_recv, - connection_sema: Arc::new(Semaphore::new(max_connections)), + connection_sema: Arc::new(Semaphore::new(plugin.max_connections)), shutdown_result: Mutex::new(None), rsa_key, public_key_der, - http_client: ReqwestClient::new(), + http_client: Default::default(), + })); + + let server = Server { + current_tick: 0, + shared, }; - Ok(SharedServer(Arc::new(server))) + let shared = server.shared.clone(); + let callbacks = plugin.callbacks.clone(); + + let start_accept_loop = move || { + let _guard = shared.tokio_handle().enter(); + + // Start accepting new connections. + tokio::spawn(do_accept_loop(shared.clone(), callbacks.clone())); + }; + + let shared = server.shared.clone(); + + // Exclusive system to spawn new clients. Should run before everything else. + let spawn_new_clients = move |world: &mut World| { + for _ in 0..shared.0.new_clients_recv.len() { + let Ok(client) = shared.0.new_clients_recv.try_recv() else { + break + }; + + world.spawn((client, Inventory::new(InventoryKind::Player))); + } + }; + + let shared = server.shared.clone(); + + // Start accepting connections in PostStartup to allow user startup code to run + // first. + app.add_startup_system_to_stage(StartupStage::PostStartup, start_accept_loop); + + // Insert resources. + app.insert_resource(server) + .insert_resource(McEntityManager::new()) + .insert_resource(PlayerList::new()); + register_client_events(&mut app.world); + + // Add core systems and stages. User code is expected to run in + // `CoreStage::Update` and `EventLoop`. + app.add_system_to_stage(CoreStage::PreUpdate, spawn_new_clients) + .add_stage_before( + CoreStage::Update, + EventLoop, + SystemStage::parallel().with_run_criteria(event_loop_run_criteria), + ) + .add_system_set_to_stage( + CoreStage::PostUpdate, + SystemSet::new() + .label("valence_core") + .with_system(init_entities) + .with_system(check_entity_invariants) + .with_system(check_instance_invariants.after(check_entity_invariants)) + .with_system(update_player_list.before(update_instances_pre_client)) + .with_system(update_instances_pre_client.after(init_entities)) + .with_system(update_clients.after(update_instances_pre_client)) + .with_system(update_instances_post_client.after(update_clients)) + .with_system(deinit_despawned_entities.after(update_instances_post_client)) + .with_system(despawn_marked_entities.after(deinit_despawned_entities)) + .with_system(update_entities.after(despawn_marked_entities)), + ) + .add_system_set_to_stage( + CoreStage::PostUpdate, + SystemSet::new() + .label("inventory") + .before("valence_core") + .with_system(handle_set_held_item) + .with_system(update_open_inventories) + .with_system(handle_close_container) + .with_system(update_client_on_close_inventory.after(update_open_inventories)) + .with_system(update_player_inventories) + .with_system( + handle_click_container + .before(update_open_inventories) + .before(update_player_inventories), + ) + .with_system( + handle_set_slot_creative + .before(update_open_inventories) + .before(update_player_inventories), + ), + ) + .add_system_to_stage(CoreStage::Last, inc_current_tick); + + let tick_duration = Duration::from_secs_f64((shared.tps() as f64).recip()); + + // Overwrite the app's runner. + app.set_runner(move |mut app: App| { + let mut app_exit_event_reader = ManualEventReader::::default(); + + loop { + let tick_start = Instant::now(); + + // Stop the server if there was an AppExit event. + if let Some(app_exit_events) = app.world.get_resource_mut::>() { + if app_exit_event_reader + .iter(&app_exit_events) + .last() + .is_some() + { + return; + } + } + + // Run the scheduled stages. + app.update(); + + // Sleep until the next tick. + thread::sleep(tick_duration.saturating_sub(tick_start.elapsed())); + } + }); + + Ok(()) +} + +/// The stage label for the special "event loop" stage. +#[derive(StageLabel)] +pub struct EventLoop; + +/// Despawns all the entities marked as despawned with the [`Despawned`] +/// component. +fn despawn_marked_entities(mut commands: Commands, entities: Query>) { + for entity in &entities { + commands.entity(entity).despawn(); + } +} + +fn inc_current_tick(mut server: ResMut) { + server.current_tick += 1; } fn make_registry_codec(dimensions: &[Dimension], biomes: &[Biome]) -> Compound { - compound! { - ident!("dimension_type") => compound! { - "type" => ident!("dimension_type"), - "value" => List::Compound(dimensions.iter().enumerate().map(|(id, dim)| compound! { + let dimensions = dimensions + .iter() + .enumerate() + .map(|(id, dim)| { + compound! { "name" => DimensionId(id as u16).dimension_type_name(), "id" => id as i32, "element" => dim.to_dimension_registry_item(), - }).collect()), + } + }) + .collect(); + + let biomes = biomes + .iter() + .enumerate() + .map(|(id, biome)| biome.to_biome_registry_item(id as i32)) + .collect(); + + compound! { + ident!("dimension_type") => compound! { + "type" => ident!("dimension_type"), + "value" => List::Compound(dimensions), }, ident!("worldgen/biome") => compound! { "type" => ident!("worldgen/biome"), "value" => { - List::Compound(biomes - .iter() - .enumerate() - .map(|(id, biome)| biome.to_biome_registry_item(id as i32)) - .collect()) + List::Compound(biomes) } }, ident!("chat_type") => compound! { @@ -431,301 +471,3 @@ fn make_registry_codec(dimensions: &[Dimension], biomes: &[Biome]) -> Compound { }, } } - -fn do_update_loop(server: &mut Server) -> ShutdownResult { - let mut tick_start = Instant::now(); - let shared = server.shared.clone(); - - let threshold = shared.0.compression_threshold; - - loop { - let _span = info_span!("update_loop", tick = server.current_tick).entered(); - - if let Some(res) = shared.0.shutdown_result.lock().unwrap().take() { - return res; - } - - for _ in 0..shared.0.new_clients_recv.len() { - let Ok(msg) = shared.0.new_clients_recv.try_recv() else { - break - }; - - info!( - username = %msg.ncd.username, - uuid = %msg.ncd.uuid, - ip = %msg.ncd.ip, - "inserting client" - ); - - server.clients.insert(Client::new( - msg.send, - msg.recv, - msg.permit, - msg.ncd, - Default::default(), - )); - } - - // Get serverbound packets first so they are not dealt with a tick late. - for (_, client) in server.clients.iter_mut() { - client.prepare_c2s_packets(); - } - - info_span!("configured_update").in_scope(|| shared.config().update(server)); - - update_entity_partition(&mut server.entities, &mut server.worlds, threshold); - - for (_, world) in server.worlds.iter_mut() { - world.chunks.update_caches(); - } - - server.player_lists.update_caches(threshold); - - server.clients.par_iter_mut().for_each(|(_, client)| { - client.update( - server.current_tick, - &shared, - &server.entities, - &server.worlds, - &server.player_lists, - &server.inventories, - ); - }); - - server.entities.update(); - - server.worlds.update(); - - server.player_lists.clear_removed(); - - server.inventories.update(); - - // Sleep for the remainder of the tick. - let tick_duration = Duration::from_secs_f64((shared.0.tick_rate as f64).recip()); - server.last_tick_duration = tick_start.elapsed(); - thread::sleep(tick_duration.saturating_sub(server.last_tick_duration)); - - tick_start = Instant::now(); - server.current_tick += 1; - } -} - -#[instrument(skip_all)] -async fn do_accept_loop(server: SharedServer) { - let listener = match TcpListener::bind(server.0.address).await { - Ok(listener) => listener, - Err(e) => { - server.shutdown(Err(e).context("failed to start TCP listener")); - return; - } - }; - - loop { - match server.0.connection_sema.clone().acquire_owned().await { - Ok(permit) => match listener.accept().await { - Ok((stream, remote_addr)) => { - tokio::spawn(handle_connection( - server.clone(), - stream, - remote_addr, - permit, - )); - } - Err(e) => { - error!("failed to accept incoming connection: {e}"); - } - }, - // Closed semaphore indicates server shutdown. - Err(_) => return, - } - } -} - -#[instrument(skip(server, stream))] -async fn handle_connection( - server: SharedServer, - stream: TcpStream, - remote_addr: SocketAddr, - permit: OwnedSemaphorePermit, -) { - trace!("handling connection"); - - if let Err(e) = stream.set_nodelay(true) { - error!("failed to set TCP_NODELAY: {e}"); - } - - let (read, write) = stream.into_split(); - - let mngr = InitialPacketManager::new( - read, - write, - PacketEncoder::new(), - PacketDecoder::new(), - Duration::from_secs(5), - permit, - ); - - // TODO: peek stream for 0xFE legacy ping - - if let Err(e) = handle_handshake(server, mngr, remote_addr).await { - // EOF can happen if the client disconnects while joining, which isn't - // very erroneous. - if let Some(e) = e.downcast_ref::() { - if e.kind() == io::ErrorKind::UnexpectedEof { - return; - } - } - warn!("connection ended with error: {e:#}"); - } -} - -async fn handle_handshake( - server: SharedServer, - mut mngr: InitialPacketManager, - remote_addr: SocketAddr, -) -> anyhow::Result<()> { - let handshake = mngr.recv_packet::().await?; - - ensure!( - matches!(server.connection_mode(), ConnectionMode::BungeeCord) - || handshake.server_address.chars().count() <= 255, - "handshake server address is too long" - ); - - match handshake.next_state { - HandshakeNextState::Status => handle_status(server, mngr, remote_addr, handshake) - .await - .context("error handling status"), - HandshakeNextState::Login => match handle_login(&server, &mut mngr, remote_addr, handshake) - .await - .context("error handling login")? - { - Some(ncd) => { - let (send, recv, permit) = mngr.into_play( - server.0.incoming_capacity, - server.0.outgoing_capacity, - server.tokio_handle().clone(), - ); - - let msg = NewClientMessage { - ncd, - send, - recv, - permit, - }; - - let _ = server.0.new_clients_send.send_async(msg).await; - Ok(()) - } - None => Ok(()), - }, - } -} - -async fn handle_status( - server: SharedServer, - mut mngr: InitialPacketManager, - remote_addr: SocketAddr, - handshake: HandshakeOwned, -) -> anyhow::Result<()> { - mngr.recv_packet::().await?; - - match server - .0 - .cfg - .server_list_ping(&server, remote_addr, handshake.protocol_version.0) - .await - { - ServerListPing::Respond { - online_players, - max_players, - player_sample, - description, - favicon_png, - } => { - let mut json = json!({ - "version": { - "name": MINECRAFT_VERSION, - "protocol": PROTOCOL_VERSION - }, - "players": { - "online": online_players, - "max": max_players, - "sample": player_sample, - }, - "description": description, - }); - - if let Some(data) = favicon_png { - let mut buf = "data:image/png;base64,".to_owned(); - base64::encode_config_buf(data, base64::STANDARD, &mut buf); - json.as_object_mut() - .unwrap() - .insert("favicon".to_owned(), Value::String(buf)); - } - - mngr.send_packet(&StatusResponse { - json: &json.to_string(), - }) - .await?; - } - ServerListPing::Ignore => return Ok(()), - } - - let PingRequest { payload } = mngr.recv_packet().await?; - - mngr.send_packet(&PingResponse { payload }).await?; - - Ok(()) -} - -/// Handle the login process and return the new client's data if successful. -async fn handle_login( - server: &SharedServer, - mngr: &mut InitialPacketManager, - remote_addr: SocketAddr, - handshake: HandshakeOwned, -) -> anyhow::Result> { - if handshake.protocol_version.0 != PROTOCOL_VERSION { - // TODO: send translated disconnect msg? - return Ok(None); - } - - let LoginStart { - username, - profile_id: _, // TODO - } = mngr.recv_packet().await?; - - let username = username.to_owned_username(); - - let ncd = match server.connection_mode() { - ConnectionMode::Online => login::online(server, mngr, remote_addr, username).await?, - ConnectionMode::Offline => login::offline(remote_addr, username)?, - ConnectionMode::BungeeCord => login::bungeecord(&handshake.server_address, username)?, - ConnectionMode::Velocity { secret } => login::velocity(mngr, username, secret).await?, - }; - - if let Some(threshold) = server.0.compression_threshold { - mngr.send_packet(&SetCompression { - threshold: VarInt(threshold as i32), - }) - .await?; - - mngr.set_compression(Some(threshold)); - } - - if let Err(reason) = server.0.cfg.login(server, &ncd).await { - info!("disconnect at login: \"{reason}\""); - mngr.send_packet(&DisconnectLogin { reason }).await?; - return Ok(None); - } - - mngr.send_packet(&LoginSuccess { - uuid: ncd.uuid, - username: ncd.username.as_str_username(), - properties: vec![], - }) - .await?; - - Ok(Some(ncd)) -} diff --git a/crates/valence/src/server/byte_channel.rs b/crates/valence/src/server/byte_channel.rs index 72973ca..ea568c9 100644 --- a/crates/valence/src/server/byte_channel.rs +++ b/crates/valence/src/server/byte_channel.rs @@ -1,3 +1,5 @@ +//! A channel specifically for sending/receiving batches of bytes. + #![allow(dead_code)] use std::sync::{Arc, Mutex}; @@ -116,6 +118,10 @@ impl ByteSender { pub fn is_disconnected(&self) -> bool { self.shared.mtx.lock().unwrap().disconnected } + + pub fn limit(&self) -> usize { + self.shared.limit + } } /// Contains any excess bytes not sent. @@ -175,6 +181,10 @@ impl ByteReceiver { pub fn is_disconnected(&self) -> bool { self.shared.mtx.lock().unwrap().disconnected } + + pub fn limit(&self) -> usize { + self.shared.limit + } } #[derive(Copy, Clone, PartialEq, Eq, Debug, Error)] diff --git a/crates/valence/src/server/connect.rs b/crates/valence/src/server/connect.rs new file mode 100644 index 0000000..616c8d0 --- /dev/null +++ b/crates/valence/src/server/connect.rs @@ -0,0 +1,503 @@ +//! Handles new connections to the server and the log-in process. + +use std::io; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{anyhow, bail, ensure, Context}; +use base64::prelude::*; +use hmac::digest::Update; +use hmac::{Hmac, Mac}; +use num::BigInt; +use reqwest::StatusCode; +use rsa::PaddingScheme; +use serde::Deserialize; +use serde_json::{json, Value}; +use sha1::Sha1; +use sha2::{Digest, Sha256}; +use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::OwnedSemaphorePermit; +use tracing::{error, info, instrument, trace, warn}; +use uuid::Uuid; +use valence_protocol::packets::c2s::handshake::HandshakeOwned; +use valence_protocol::packets::c2s::login::{EncryptionResponse, LoginPluginResponse, LoginStart}; +use valence_protocol::packets::c2s::status::{PingRequest, StatusRequest}; +use valence_protocol::packets::s2c::login::{ + DisconnectLogin, EncryptionRequest, LoginPluginRequest, LoginSuccess, SetCompression, +}; +use valence_protocol::packets::s2c::status::{PingResponse, StatusResponse}; +use valence_protocol::types::{HandshakeNextState, Property}; +use valence_protocol::{ + translation_key, Decode, Ident, PacketDecoder, PacketEncoder, RawBytes, Text, Username, VarInt, + MINECRAFT_VERSION, PROTOCOL_VERSION, +}; + +use crate::config::{AsyncCallbacks, ConnectionMode, ServerListPing}; +use crate::server::connection::InitialConnection; +use crate::server::{NewClientInfo, SharedServer}; + +/// Accepts new connections to the server as they occur. +#[instrument(skip_all)] +pub async fn do_accept_loop(shared: SharedServer, callbacks: Arc) { + let listener = match TcpListener::bind(shared.0.address).await { + Ok(listener) => listener, + Err(e) => { + shared.shutdown(Err(e).context("failed to start TCP listener")); + return; + } + }; + + loop { + match shared.0.connection_sema.clone().acquire_owned().await { + Ok(permit) => match listener.accept().await { + Ok((stream, remote_addr)) => { + tokio::spawn(handle_connection( + shared.clone(), + callbacks.clone(), + stream, + remote_addr, + permit, + )); + } + Err(e) => { + error!("failed to accept incoming connection: {e}"); + } + }, + // Closed semaphore indicates server shutdown. + Err(_) => return, + } + } +} + +#[instrument(skip(shared, callbacks, stream, permit))] +async fn handle_connection( + shared: SharedServer, + callbacks: Arc, + stream: TcpStream, + remote_addr: SocketAddr, + permit: OwnedSemaphorePermit, +) { + trace!("handling connection"); + + if let Err(e) = stream.set_nodelay(true) { + error!("failed to set TCP_NODELAY: {e}"); + } + + let (read, write) = stream.into_split(); + + let conn = InitialConnection::new( + read, + write, + PacketEncoder::new(), + PacketDecoder::new(), + Duration::from_secs(5), + permit, + ); + + // TODO: peek stream for 0xFE legacy ping + + if let Err(e) = handle_handshake(shared, callbacks, conn, remote_addr).await { + // EOF can happen if the client disconnects while joining, which isn't + // very erroneous. + if let Some(e) = e.downcast_ref::() { + if e.kind() == io::ErrorKind::UnexpectedEof { + return; + } + } + warn!("connection ended with error: {e:#}"); + } +} + +async fn handle_handshake( + shared: SharedServer, + callbacks: Arc, + mut conn: InitialConnection, + remote_addr: SocketAddr, +) -> anyhow::Result<()> { + let handshake = conn.recv_packet::().await?; + + ensure!( + matches!(shared.connection_mode(), ConnectionMode::BungeeCord) + || handshake.server_address.chars().count() <= 255, + "handshake server address is too long" + ); + + match handshake.next_state { + HandshakeNextState::Status => { + handle_status(shared, callbacks, conn, remote_addr, handshake) + .await + .context("error handling status") + } + HandshakeNextState::Login => { + match handle_login(&shared, callbacks, &mut conn, remote_addr, handshake) + .await + .context("error handling login")? + { + Some(info) => { + let client = conn.into_client( + info, + shared.0.incoming_capacity, + shared.0.outgoing_capacity, + ); + + let _ = shared.0.new_clients_send.send_async(client).await; + + Ok(()) + } + None => Ok(()), + } + } + } +} + +async fn handle_status( + shared: SharedServer, + callbacks: Arc, + mut conn: InitialConnection, + remote_addr: SocketAddr, + handshake: HandshakeOwned, +) -> anyhow::Result<()> { + conn.recv_packet::().await?; + + match callbacks + .server_list_ping(&shared, remote_addr, handshake.protocol_version.0) + .await + { + ServerListPing::Respond { + online_players, + max_players, + player_sample, + description, + favicon_png, + } => { + let mut json = json!({ + "version": { + "name": MINECRAFT_VERSION, + "protocol": PROTOCOL_VERSION + }, + "players": { + "online": online_players, + "max": max_players, + "sample": player_sample, + }, + "description": description, + }); + + if !favicon_png.is_empty() { + let mut buf = "data:image/png;base64,".to_owned(); + BASE64_STANDARD.encode_string(favicon_png, &mut buf); + json["favicon"] = Value::String(buf); + } + + conn.send_packet(&StatusResponse { + json: &json.to_string(), + }) + .await?; + } + ServerListPing::Ignore => return Ok(()), + } + + let PingRequest { payload } = conn.recv_packet().await?; + + conn.send_packet(&PingResponse { payload }).await?; + + Ok(()) +} + +/// Handle the login process and return the new client's data if successful. +async fn handle_login( + shared: &SharedServer, + callbacks: Arc, + conn: &mut InitialConnection, + remote_addr: SocketAddr, + handshake: HandshakeOwned, +) -> anyhow::Result> { + if handshake.protocol_version.0 != PROTOCOL_VERSION { + // TODO: send translated disconnect msg? + return Ok(None); + } + + let LoginStart { + username, + profile_id: _, // TODO + } = conn.recv_packet().await?; + + let username = username.to_owned_username(); + + let info = match shared.connection_mode() { + ConnectionMode::Online { .. } => { + login_online(shared, &callbacks, conn, remote_addr, username).await? + } + ConnectionMode::Offline => login_offline(remote_addr, username)?, + ConnectionMode::BungeeCord => login_bungeecord(&handshake.server_address, username)?, + ConnectionMode::Velocity { secret } => login_velocity(conn, username, secret).await?, + }; + + if let Some(threshold) = shared.0.compression_threshold { + conn.send_packet(&SetCompression { + threshold: VarInt(threshold as i32), + }) + .await?; + + conn.set_compression(Some(threshold)); + } + + if let Err(reason) = callbacks.login(shared, &info).await { + info!("disconnect at login: \"{reason}\""); + conn.send_packet(&DisconnectLogin { + reason: reason.into(), + }) + .await?; + return Ok(None); + } + + conn.send_packet(&LoginSuccess { + uuid: info.uuid, + username: info.username.as_str_username(), + properties: Default::default(), + }) + .await?; + + Ok(Some(info)) +} + +/// Login procedure for online mode. +pub(super) async fn login_online( + shared: &SharedServer, + callbacks: &Arc, + conn: &mut InitialConnection, + remote_addr: SocketAddr, + username: Username, +) -> anyhow::Result { + let my_verify_token: [u8; 16] = rand::random(); + + conn.send_packet(&EncryptionRequest { + server_id: "", // Always empty + public_key: &shared.0.public_key_der, + verify_token: &my_verify_token, + }) + .await?; + + let EncryptionResponse { + shared_secret, + verify_token: encrypted_verify_token, + } = conn.recv_packet().await?; + + let shared_secret = shared + .0 + .rsa_key + .decrypt(PaddingScheme::PKCS1v15Encrypt, shared_secret) + .context("failed to decrypt shared secret")?; + + let verify_token = shared + .0 + .rsa_key + .decrypt(PaddingScheme::PKCS1v15Encrypt, encrypted_verify_token) + .context("failed to decrypt verify token")?; + + ensure!( + my_verify_token.as_slice() == verify_token, + "verify tokens do not match" + ); + + let crypt_key: [u8; 16] = shared_secret + .as_slice() + .try_into() + .context("shared secret has the wrong length")?; + + conn.enable_encryption(&crypt_key); + + let hash = Sha1::new() + .chain(&shared_secret) + .chain(&shared.0.public_key_der) + .finalize(); + + let url = callbacks + .session_server( + shared, + username.as_str_username(), + &auth_digest(&hash), + &remote_addr.ip(), + ) + .await; + + let resp = shared.0.http_client.get(url).send().await?; + + match resp.status() { + StatusCode::OK => {} + StatusCode::NO_CONTENT => { + let reason = Text::translate( + translation_key::MULTIPLAYER_DISCONNECT_UNVERIFIED_USERNAME, + [], + ); + conn.send_packet(&DisconnectLogin { + reason: reason.into(), + }) + .await?; + bail!("session server could not verify username"); + } + status => { + bail!("session server GET request failed (status code {status})"); + } + } + + #[derive(Debug, Deserialize)] + struct GameProfile { + id: Uuid, + name: Username, + properties: Vec, + } + + let profile: GameProfile = resp.json().await.context("parsing game profile")?; + + ensure!(profile.name == username, "usernames do not match"); + + Ok(NewClientInfo { + uuid: profile.id, + username, + ip: remote_addr.ip(), + properties: profile.properties, + }) +} + +fn auth_digest(bytes: &[u8]) -> String { + BigInt::from_signed_bytes_be(bytes).to_str_radix(16) +} + +/// Login procedure for offline mode. +pub(super) fn login_offline( + remote_addr: SocketAddr, + username: Username, +) -> anyhow::Result { + Ok(NewClientInfo { + // Derive the client's UUID from a hash of their username. + uuid: Uuid::from_slice(&Sha256::digest(username.as_str())[..16])?, + username, + properties: vec![], + ip: remote_addr.ip(), + }) +} + +/// Login procedure for BungeeCord. +pub(super) fn login_bungeecord( + server_address: &str, + username: Username, +) -> anyhow::Result { + // Get data from server_address field of the handshake + let [_, client_ip, uuid, properties]: [&str; 4] = server_address + .split('\0') + .take(4) + .collect::>() + .try_into() + .map_err(|_| anyhow!("malformed BungeeCord server address data"))?; + + // Read properties and get textures + let properties: Vec = + serde_json::from_str(properties).context("failed to parse BungeeCord player properties")?; + + Ok(NewClientInfo { + uuid: uuid.parse()?, + username, + properties, + ip: client_ip.parse()?, + }) +} + +/// Login procedure for Velocity. +pub(super) async fn login_velocity( + conn: &mut InitialConnection, + username: Username, + velocity_secret: &str, +) -> anyhow::Result { + const VELOCITY_MIN_SUPPORTED_VERSION: u8 = 1; + const VELOCITY_MODERN_FORWARDING_WITH_KEY_V2: i32 = 3; + + let message_id: i32 = 0; // TODO: make this random? + + // Send Player Info Request into the Plugin Channel + conn.send_packet(&LoginPluginRequest { + message_id: VarInt(message_id), + channel: Ident::new("velocity:player_info").unwrap(), + data: RawBytes(&[VELOCITY_MIN_SUPPORTED_VERSION]), + }) + .await?; + + // Get Response + let plugin_response: LoginPluginResponse = conn.recv_packet().await?; + + ensure!( + plugin_response.message_id.0 == message_id, + "mismatched plugin response ID (got {}, expected {message_id})", + plugin_response.message_id.0, + ); + + let data = plugin_response + .data + .context("missing plugin response data")? + .0; + + ensure!(data.len() >= 32, "invalid plugin response data length"); + let (signature, mut data_without_signature) = data.split_at(32); + + // Verify signature + let mut mac = Hmac::::new_from_slice(velocity_secret.as_bytes())?; + Mac::update(&mut mac, data_without_signature); + mac.verify_slice(signature)?; + + // Check Velocity version + let version = VarInt::decode(&mut data_without_signature) + .context("failed to decode velocity version")? + .0; + + // Get client address + let remote_addr = String::decode(&mut data_without_signature)?.parse()?; + + // Get UUID + let uuid = Uuid::decode(&mut data_without_signature)?; + + // Get username and validate + ensure!( + username == Username::decode(&mut data_without_signature)?, + "mismatched usernames" + ); + + // Read game profile properties + let properties = Vec::::decode(&mut data_without_signature) + .context("decoding velocity game profile properties")?; + + if version >= VELOCITY_MODERN_FORWARDING_WITH_KEY_V2 { + // TODO + } + + Ok(NewClientInfo { + uuid, + username, + properties, + ip: remote_addr, + }) +} + +#[cfg(test)] +mod tests { + use sha1::Digest; + + use super::*; + + #[test] + fn auth_digest_usernames() { + assert_eq!( + auth_digest(&Sha1::digest("Notch")), + "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48" + ); + assert_eq!( + auth_digest(&Sha1::digest("jeb_")), + "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1" + ); + assert_eq!( + auth_digest(&Sha1::digest("simon")), + "88e16a1019277b15d58faf0541e11910eb756f6" + ); + } +} diff --git a/crates/valence/src/server/packet_manager.rs b/crates/valence/src/server/connection.rs similarity index 61% rename from crates/valence/src/server/packet_manager.rs rename to crates/valence/src/server/connection.rs index d20aa06..f0d610a 100644 --- a/crates/valence/src/server/packet_manager.rs +++ b/crates/valence/src/server/connection.rs @@ -1,21 +1,23 @@ -use std::fmt; +use std::io; use std::io::ErrorKind; use std::time::Duration; -use anyhow::Result; -use tokio::io; +use anyhow::bail; +use bytes::BytesMut; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tokio::runtime::Handle; use tokio::sync::OwnedSemaphorePermit; use tokio::task::JoinHandle; use tokio::time::timeout; use tracing::debug; use valence_protocol::{DecodePacket, EncodePacket, PacketDecoder, PacketEncoder}; -use crate::packet::WritePacket; -use crate::server::byte_channel::{byte_channel, ByteReceiver, ByteSender, TryRecvError}; +use crate::client::{Client, ClientConnection}; +use crate::server::byte_channel::{ + byte_channel, ByteReceiver, ByteSender, TryRecvError, TrySendError, +}; +use crate::server::NewClientInfo; -pub struct InitialPacketManager { +pub(super) struct InitialConnection { reader: R, writer: W, enc: PacketEncoder, @@ -26,7 +28,7 @@ pub struct InitialPacketManager { const READ_BUF_SIZE: usize = 4096; -impl InitialPacketManager +impl InitialConnection where R: AsyncRead + Unpin, W: AsyncWrite + Unpin, @@ -49,7 +51,7 @@ where } } - pub async fn send_packet

(&mut self, pkt: &P) -> Result<()> + pub async fn send_packet

(&mut self, pkt: &P) -> anyhow::Result<()> where P: EncodePacket + ?Sized, { @@ -59,9 +61,9 @@ where Ok(()) } - pub async fn recv_packet<'a, P>(&'a mut self) -> Result

+ pub async fn recv_packet<'a, P>(&'a mut self) -> anyhow::Result

where - P: DecodePacket<'a> + fmt::Debug, + P: DecodePacket<'a>, { timeout(self.timeout, async { while !self.dec.has_next_packet()? { @@ -117,12 +119,12 @@ where self.dec.enable_encryption(key); } - pub fn into_play( + pub fn into_client( mut self, + info: NewClientInfo, incoming_limit: usize, outgoing_limit: usize, - handle: Handle, - ) -> (PlayPacketSender, PlayPacketReceiver, OwnedSemaphorePermit) + ) -> Client where R: Send + 'static, W: Send + 'static, @@ -168,118 +170,55 @@ where } }); - ( - PlayPacketSender { - enc: self.enc, + Client::new( + info, + Box::new(RealClientConnection { send: outgoing_sender, - writer_task: Some(writer_task), - handle, - }, - PlayPacketReceiver { - dec: self.dec, recv: incoming_receiver, + _permit: self.permit, reader_task, - }, - self.permit, + writer_task, + }), + self.enc, + self.dec, ) } } -/// Manages a packet encoder and a byte channel to send the encoded packets -/// through. -pub struct PlayPacketSender { - enc: PacketEncoder, +struct RealClientConnection { send: ByteSender, - writer_task: Option>, - handle: Handle, -} - -impl PlayPacketSender { - pub fn append_packet

(&mut self, pkt: &P) -> Result<()> - where - P: EncodePacket + ?Sized, - { - self.enc.append_packet(pkt) - } - - pub fn append_bytes(&mut self, bytes: &[u8]) { - self.enc.append_bytes(bytes) - } - - pub fn prepend_packet

(&mut self, pkt: &P) -> Result<()> - where - P: EncodePacket + ?Sized, - { - self.enc.prepend_packet(pkt) - } - - pub fn flush(&mut self) -> Result<()> { - let bytes = self.enc.take(); - self.send.try_send(bytes)?; - Ok(()) - } -} - -impl WritePacket for PlayPacketSender { - fn write_packet

(&mut self, packet: &P) -> Result<()> - where - P: EncodePacket + ?Sized, - { - self.append_packet(packet) - } - - fn write_bytes(&mut self, bytes: &[u8]) -> Result<()> { - self.append_bytes(bytes); - Ok(()) - } -} - -impl Drop for PlayPacketSender { - fn drop(&mut self) { - let _ = self.flush(); - - if let Some(writer_task) = self.writer_task.take() { - if !writer_task.is_finished() { - let _guard = self.handle.enter(); - - // Give any unsent packets a moment to send before we cut the connection. - self.handle - .spawn(timeout(Duration::from_secs(1), writer_task)); - } - } - } -} - -/// Manages a packet decoder and a byte channel to receive the encoded packets. -pub struct PlayPacketReceiver { - dec: PacketDecoder, recv: ByteReceiver, + /// Ensures that we don't allow more connections to the server until the + /// client is dropped. + _permit: OwnedSemaphorePermit, reader_task: JoinHandle<()>, + writer_task: JoinHandle<()>, } -impl PlayPacketReceiver { - pub fn try_next_packet<'a, P>(&'a mut self) -> Result> - where - P: DecodePacket<'a> + fmt::Debug, - { - self.dec.try_next_packet() - } - - /// Returns true if the client is connected. Returns false otherwise. - pub fn try_recv(&mut self) -> bool { - match self.recv.try_recv() { - Ok(bytes) => { - self.dec.queue_bytes(bytes); - true - } - Err(TryRecvError::Empty) => true, - Err(TryRecvError::Disconnected) => false, - } - } -} - -impl Drop for PlayPacketReceiver { +impl Drop for RealClientConnection { fn drop(&mut self) { + self.writer_task.abort(); self.reader_task.abort(); } } + +impl ClientConnection for RealClientConnection { + fn try_send(&mut self, bytes: BytesMut) -> anyhow::Result<()> { + match self.send.try_send(bytes) { + Ok(()) => Ok(()), + Err(TrySendError::Full(_)) => bail!( + "reached configured outgoing limit of {} bytes", + self.send.limit() + ), + Err(TrySendError::Disconnected(_)) => bail!("client disconnected"), + } + } + + fn try_recv(&mut self) -> anyhow::Result { + match self.recv.try_recv() { + Ok(bytes) => Ok(bytes), + Err(TryRecvError::Empty) => Ok(BytesMut::new()), + Err(TryRecvError::Disconnected) => bail!("client disconnected"), + } + } +} diff --git a/crates/valence/src/server/login.rs b/crates/valence/src/server/login.rs deleted file mode 100644 index 6f5da05..0000000 --- a/crates/valence/src/server/login.rs +++ /dev/null @@ -1,302 +0,0 @@ -//! Contains login procedures for the different [`ConnectionMode`]s. -//! -//! [`ConnectionMode`]: crate::config::ConnectionMode - -use std::net::SocketAddr; - -use anyhow::{anyhow, bail, ensure, Context}; -use hmac::digest::Update; -use hmac::{Hmac, Mac}; -use num::BigInt; -use reqwest::StatusCode; -use rsa::PaddingScheme; -use serde::Deserialize; -use sha1::Sha1; -use sha2::{Digest, Sha256}; -use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; -use uuid::Uuid; -use valence_protocol::packets::c2s::login::{EncryptionResponse, LoginPluginResponse}; -use valence_protocol::packets::s2c::login::{ - DisconnectLogin, EncryptionRequest, LoginPluginRequest, -}; -use valence_protocol::types::{SignedProperty, SignedPropertyOwned}; -use valence_protocol::{translation_key, Decode, Ident, RawBytes, Text, Username, VarInt}; - -use crate::config::Config; -use crate::player_textures::SignedPlayerTextures; -use crate::server::packet_manager::InitialPacketManager; -use crate::server::{NewClientData, SharedServer}; - -/// Login sequence for -/// [`ConnectionMode::Online`](crate::config::ConnectionMode). -pub(super) async fn online( - server: &SharedServer, - mngr: &mut InitialPacketManager, - remote_addr: SocketAddr, - username: Username, -) -> anyhow::Result { - let my_verify_token: [u8; 16] = rand::random(); - - mngr.send_packet(&EncryptionRequest { - server_id: "", // Always empty - public_key: &server.0.public_key_der, - verify_token: &my_verify_token, - }) - .await?; - - let EncryptionResponse { - shared_secret, - verify_token: encrypted_verify_token, - } = mngr.recv_packet().await?; - - let shared_secret = server - .0 - .rsa_key - .decrypt(PaddingScheme::PKCS1v15Encrypt, shared_secret) - .context("failed to decrypt shared secret")?; - - let verify_token = server - .0 - .rsa_key - .decrypt(PaddingScheme::PKCS1v15Encrypt, encrypted_verify_token) - .context("failed to decrypt verify token")?; - - ensure!( - my_verify_token.as_slice() == verify_token, - "verify tokens do not match" - ); - - let crypt_key: [u8; 16] = shared_secret - .as_slice() - .try_into() - .context("shared secret has the wrong length")?; - - mngr.enable_encryption(&crypt_key); - - let hash = Sha1::new() - .chain(&shared_secret) - .chain(&server.0.public_key_der) - .finalize(); - - let url = server.config().session_server( - server, - username.as_str_username(), - &auth_digest(&hash), - &remote_addr.ip(), - ); - - let resp = server.0.http_client.get(url).send().await?; - - match resp.status() { - StatusCode::OK => {} - StatusCode::NO_CONTENT => { - let reason = Text::translate( - translation_key::MULTIPLAYER_DISCONNECT_UNVERIFIED_USERNAME, - [], - ); - mngr.send_packet(&DisconnectLogin { reason }).await?; - bail!("session server could not verify username"); - } - status => { - bail!("session server GET request failed (status code {status})"); - } - } - - #[derive(Debug, Deserialize)] - struct AuthResponse { - id: String, - name: Username, - properties: Vec, - } - - let data: AuthResponse = resp.json().await?; - - ensure!(data.name == username, "usernames do not match"); - - let uuid = Uuid::parse_str(&data.id).context("failed to parse player's UUID")?; - - let textures = match data.properties.into_iter().find(|p| p.name == "textures") { - Some(p) => SignedPlayerTextures::from_base64( - p.value, - p.signature.context("missing signature for textures")?, - )?, - None => bail!("failed to find textures in auth response"), - }; - - Ok(NewClientData { - uuid, - username, - ip: remote_addr.ip(), - textures: Some(textures), - }) -} - -/// Login sequence for -/// [`ConnectionMode::Offline`](crate::config::ConnectionMode). -pub(super) fn offline( - remote_addr: SocketAddr, - username: Username, -) -> anyhow::Result { - Ok(NewClientData { - // Derive the client's UUID from a hash of their username. - uuid: Uuid::from_slice(&Sha256::digest(username.as_str())[..16])?, - username, - textures: None, - ip: remote_addr.ip(), - }) -} - -/// Login sequence for -/// [`ConnectionMode::BungeeCord`](crate::config::ConnectionMode). -pub(super) fn bungeecord( - server_address: &str, - username: Username, -) -> anyhow::Result { - // Get data from server_address field of the handshake - let [_, client_ip, uuid, properties]: [&str; 4] = server_address - .split('\0') - .take(4) - .collect::>() - .try_into() - .map_err(|_| anyhow!("malformed BungeeCord server address data"))?; - - // Read properties and get textures - let properties: Vec = - serde_json::from_str(properties).context("failed to parse BungeeCord player properties")?; - - let mut textures = None; - for prop in properties { - if prop.name == "textures" { - textures = Some( - SignedPlayerTextures::from_base64( - prop.value, - prop.signature - .context("missing player textures signature")?, - ) - .context("failed to parse signed player textures")?, - ); - break; - } - } - - Ok(NewClientData { - uuid: uuid.parse()?, - username, - textures, - ip: client_ip.parse()?, - }) -} - -fn auth_digest(bytes: &[u8]) -> String { - BigInt::from_signed_bytes_be(bytes).to_str_radix(16) -} - -pub(super) async fn velocity( - mngr: &mut InitialPacketManager, - username: Username, - velocity_secret: &str, -) -> anyhow::Result { - const VELOCITY_MIN_SUPPORTED_VERSION: u8 = 1; - const VELOCITY_MODERN_FORWARDING_WITH_KEY_V2: i32 = 3; - - let message_id: i32 = 0; // TODO: make this random? - - // Send Player Info Request into the Plugin Channel - mngr.send_packet(&LoginPluginRequest { - message_id: VarInt(message_id), - channel: Ident::new("velocity:player_info").unwrap(), - data: RawBytes(&[VELOCITY_MIN_SUPPORTED_VERSION]), - }) - .await?; - - // Get Response - let plugin_response: LoginPluginResponse = mngr.recv_packet().await?; - - ensure!( - plugin_response.message_id.0 == message_id, - "mismatched plugin response ID (got {}, expected {message_id})", - plugin_response.message_id.0, - ); - - let data = plugin_response - .data - .context("missing plugin response data")? - .0; - - ensure!(data.len() >= 32, "invalid plugin response data length"); - let (signature, mut data_without_signature) = data.split_at(32); - - // Verify signature - let mut mac = Hmac::::new_from_slice(velocity_secret.as_bytes())?; - Mac::update(&mut mac, data_without_signature); - mac.verify_slice(signature)?; - - // Check Velocity version - let version = VarInt::decode(&mut data_without_signature) - .context("failed to decode velocity version")? - .0; - - // Get client address - let remote_addr = String::decode(&mut data_without_signature)?.parse()?; - - // Get UUID - let uuid = Uuid::decode(&mut data_without_signature)?; - - // Get username and validate - ensure!( - username == Username::decode(&mut data_without_signature)?, - "mismatched usernames" - ); - - // Read properties and get textures - let mut textures = None; - for prop in Vec::::decode(&mut data_without_signature) - .context("failed to decode velocity player properties")? - { - if prop.name == "textures" { - textures = Some( - SignedPlayerTextures::from_base64( - prop.value, - prop.signature - .context("missing player textures signature")?, - ) - .context("failed to parse signed player textures")?, - ); - break; - } - } - - if version >= VELOCITY_MODERN_FORWARDING_WITH_KEY_V2 { - // TODO - } - - Ok(NewClientData { - uuid, - username, - textures, - ip: remote_addr, - }) -} - -#[cfg(test)] -mod tests { - use sha1::Digest; - - use super::*; - - #[test] - fn auth_digest_usernames() { - assert_eq!( - auth_digest(&Sha1::digest("Notch")), - "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48" - ); - assert_eq!( - auth_digest(&Sha1::digest("jeb_")), - "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1" - ); - assert_eq!( - auth_digest(&Sha1::digest("simon")), - "88e16a1019277b15d58faf0541e11910eb756f6" - ); - } -} diff --git a/crates/valence/src/slab.rs b/crates/valence/src/slab.rs deleted file mode 100644 index cb90368..0000000 --- a/crates/valence/src/slab.rs +++ /dev/null @@ -1,340 +0,0 @@ -use std::iter::FusedIterator; -use std::{iter, mem, slice}; - -use rayon::iter::plumbing::UnindexedConsumer; -use rayon::prelude::*; - -#[derive(Clone, Debug)] -pub struct Slab { - entries: Vec>, - next_free_head: usize, - len: usize, -} - -impl Default for Slab { - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Debug)] -enum Entry { - Occupied(T), - Vacant { next_free: usize }, -} - -impl Slab { - pub const fn new() -> Self { - Self { - entries: vec![], - next_free_head: 0, - len: 0, - } - } - - pub fn get(&self, key: usize) -> Option<&T> { - match self.entries.get(key)? { - Entry::Occupied(value) => Some(value), - Entry::Vacant { .. } => None, - } - } - - pub fn get_mut(&mut self, key: usize) -> Option<&mut T> { - match self.entries.get_mut(key)? { - Entry::Occupied(value) => Some(value), - Entry::Vacant { .. } => None, - } - } - - pub fn len(&self) -> usize { - self.len - } - - pub fn insert(&mut self, value: T) -> (usize, &mut T) { - self.insert_with(|_| value) - } - - pub fn insert_with(&mut self, f: impl FnOnce(usize) -> T) -> (usize, &mut T) { - self.len += 1; - - if self.next_free_head == self.entries.len() { - let key = self.next_free_head; - - self.next_free_head += 1; - - self.entries.push(Entry::Occupied(f(key))); - - match self.entries.last_mut() { - Some(Entry::Occupied(value)) => (key, value), - _ => unreachable!(), - } - } else { - let entry = &mut self.entries[self.next_free_head]; - - let next_free = match entry { - Entry::Occupied(_) => unreachable!("corrupt free list"), - Entry::Vacant { next_free } => *next_free, - }; - - let key = self.next_free_head; - - *entry = Entry::Occupied(f(key)); - - self.next_free_head = next_free; - - match entry { - Entry::Occupied(value) => (key, value), - Entry::Vacant { .. } => unreachable!(), - } - } - } - - pub fn remove(&mut self, key: usize) -> Option { - let entry = self.entries.get_mut(key)?; - match entry { - Entry::Occupied(_) => { - let old_entry = mem::replace( - entry, - Entry::Vacant { - next_free: self.next_free_head, - }, - ); - - self.next_free_head = key; - self.len -= 1; - - match old_entry { - Entry::Occupied(value) => Some(value), - Entry::Vacant { .. } => unreachable!(), - } - } - Entry::Vacant { .. } => None, - } - } - - pub fn retain(&mut self, mut f: impl FnMut(usize, &mut T) -> bool) { - for (key, entry) in self.entries.iter_mut().enumerate() { - if let Entry::Occupied(value) = entry { - if !f(key, value) { - *entry = Entry::Vacant { - next_free: self.next_free_head, - }; - - self.next_free_head = key; - self.len -= 1; - } - } - } - } - - pub fn clear(&mut self) { - self.entries.clear(); - self.next_free_head = 0; - self.len = 0; - } - - pub fn iter(&self) -> Iter { - Iter { - entries: self.entries.iter().enumerate(), - len: self.len, - } - } - - pub fn iter_mut(&mut self) -> IterMut { - IterMut { - entries: self.entries.iter_mut().enumerate(), - len: self.len, - } - } -} - -impl<'a, T> IntoIterator for &'a Slab { - type Item = (usize, &'a T); - type IntoIter = Iter<'a, T>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl<'a, T> IntoIterator for &'a mut Slab { - type Item = (usize, &'a mut T); - type IntoIter = IterMut<'a, T>; - - fn into_iter(self) -> Self::IntoIter { - self.iter_mut() - } -} - -impl<'a, T: Sync> IntoParallelIterator for &'a Slab { - type Iter = ParIter<'a, T>; - type Item = (usize, &'a T); - - fn into_par_iter(self) -> Self::Iter { - ParIter { slab: self } - } -} - -impl<'a, T: Send + Sync> IntoParallelIterator for &'a mut Slab { - type Iter = ParIterMut<'a, T>; - type Item = (usize, &'a mut T); - - fn into_par_iter(self) -> Self::Iter { - ParIterMut { slab: self } - } -} - -pub struct Iter<'a, T> { - entries: iter::Enumerate>>, - len: usize, -} - -pub struct IterMut<'a, T> { - entries: iter::Enumerate>>, - len: usize, -} - -pub struct ParIter<'a, T> { - slab: &'a Slab, -} - -pub struct ParIterMut<'a, T> { - slab: &'a mut Slab, -} - -impl<'a, T> Clone for Iter<'a, T> { - fn clone(&self) -> Self { - Self { - entries: self.entries.clone(), - len: self.len, - } - } -} - -impl<'a, T> Iterator for Iter<'a, T> { - type Item = (usize, &'a T); - - fn next(&mut self) -> Option { - for (key, entry) in &mut self.entries { - if let Entry::Occupied(value) = entry { - self.len -= 1; - return Some((key, value)); - } - } - - debug_assert_eq!(self.len, 0); - None - } - - fn size_hint(&self) -> (usize, Option) { - (self.len, Some(self.len)) - } -} - -impl DoubleEndedIterator for Iter<'_, T> { - fn next_back(&mut self) -> Option { - while let Some((key, entry)) = self.entries.next_back() { - if let Entry::Occupied(value) = entry { - self.len -= 1; - return Some((key, value)); - } - } - - debug_assert_eq!(self.len, 0); - None - } -} - -impl ExactSizeIterator for Iter<'_, T> { - fn len(&self) -> usize { - self.len - } -} - -impl FusedIterator for Iter<'_, T> {} - -impl<'a, T> Iterator for IterMut<'a, T> { - type Item = (usize, &'a mut T); - - fn next(&mut self) -> Option { - for (key, entry) in &mut self.entries { - if let Entry::Occupied(value) = entry { - self.len -= 1; - return Some((key, value)); - } - } - - debug_assert_eq!(self.len, 0); - None - } - - fn size_hint(&self) -> (usize, Option) { - (self.len, Some(self.len)) - } -} - -impl DoubleEndedIterator for IterMut<'_, T> { - fn next_back(&mut self) -> Option { - while let Some((key, entry)) = self.entries.next_back() { - if let Entry::Occupied(value) = entry { - self.len -= 1; - return Some((key, value)); - } - } - - debug_assert_eq!(self.len, 0); - None - } -} - -impl ExactSizeIterator for IterMut<'_, T> { - fn len(&self) -> usize { - self.len - } -} - -impl FusedIterator for IterMut<'_, T> {} - -impl Clone for ParIter<'_, T> { - fn clone(&self) -> Self { - Self { slab: self.slab } - } -} - -impl<'a, T: Sync> ParallelIterator for ParIter<'a, T> { - type Item = (usize, &'a T); - - fn drive_unindexed(self, consumer: C) -> C::Result - where - C: UnindexedConsumer, - { - self.slab - .entries - .par_iter() - .enumerate() - .filter_map(|(key, value)| match value { - Entry::Occupied(value) => Some((key, value)), - Entry::Vacant { .. } => None, - }) - .drive_unindexed(consumer) - } -} - -impl<'a, T: Send + Sync> ParallelIterator for ParIterMut<'a, T> { - type Item = (usize, &'a mut T); - - fn drive_unindexed(self, consumer: C) -> C::Result - where - C: UnindexedConsumer, - { - self.slab - .entries - .par_iter_mut() - .enumerate() - .filter_map(|(key, value)| match value { - Entry::Occupied(value) => Some((key, value)), - Entry::Vacant { .. } => None, - }) - .drive_unindexed(consumer) - } -} diff --git a/crates/valence/src/slab_rc.rs b/crates/valence/src/slab_rc.rs deleted file mode 100644 index da11621..0000000 --- a/crates/valence/src/slab_rc.rs +++ /dev/null @@ -1,128 +0,0 @@ -#![allow(dead_code)] - -use std::cmp::Ordering; -use std::hash::{Hash, Hasher}; -use std::iter::FusedIterator; -use std::sync::Arc; - -use flume::{Receiver, Sender}; - -#[derive(Debug)] -pub struct RcSlab { - entries: Vec, - free_send: Sender, - free_recv: Receiver, -} - -impl RcSlab { - pub fn new() -> Self { - let (free_send, free_recv) = flume::unbounded(); - - Self { - entries: vec![], - free_send, - free_recv, - } - } - - pub fn insert(&mut self, value: T) -> (Key, &mut T) { - match self.free_recv.try_recv() { - Ok(idx) => { - self.entries[idx] = value; - let k = Key(Arc::new(KeyInner { - index: idx, - free_send: self.free_send.clone(), - })); - - (k, &mut self.entries[idx]) - } - Err(_) => { - let idx = self.entries.len(); - self.entries.push(value); - let k = Key(Arc::new(KeyInner { - index: idx, - free_send: self.free_send.clone(), - })); - (k, &mut self.entries[idx]) - } - } - } - - pub fn get(&self, key: &Key) -> &T { - &self.entries[key.0.index] - } - - pub fn get_mut(&mut self, key: &Key) -> &mut T { - &mut self.entries[key.0.index] - } - - pub fn iter(&self) -> impl FusedIterator + Clone + '_ { - self.entries.iter() - } - - pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { - self.entries.iter_mut() - } -} - -#[derive(Clone, Debug)] -pub struct Key(Arc); - -#[derive(Debug)] -struct KeyInner { - index: usize, - free_send: Sender, -} - -impl Drop for KeyInner { - fn drop(&mut self) { - let _ = self.free_send.send(self.index); - } -} - -impl PartialEq for Key { - fn eq(&self, other: &Self) -> bool { - Arc::as_ptr(&self.0) == Arc::as_ptr(&other.0) - } -} - -impl Eq for Key {} - -impl PartialOrd for Key { - fn partial_cmp(&self, other: &Self) -> Option { - Arc::as_ptr(&self.0).partial_cmp(&Arc::as_ptr(&other.0)) - } -} - -impl Ord for Key { - fn cmp(&self, other: &Self) -> Ordering { - self.partial_cmp(other).unwrap() - } -} - -impl Hash for Key { - fn hash(&self, state: &mut H) { - Arc::as_ptr(&self.0).hash(state) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - - #[test] - fn rc_slab_insert() { - let mut slab = RcSlab::new(); - let (k, v) = slab.insert(123); - assert_eq!(*v, 123); - - let k2 = slab.insert(456).0; - assert_ne!(k, k2); - assert_eq!(slab.entries.len(), 2); - - drop(k); - drop(k2); - slab.insert(789); - assert_eq!(slab.entries.len(), 2); - } -} diff --git a/crates/valence/src/slab_versioned.rs b/crates/valence/src/slab_versioned.rs deleted file mode 100644 index a9c5ba3..0000000 --- a/crates/valence/src/slab_versioned.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::iter::FusedIterator; -use std::num::NonZeroU32; - -use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator}; -use tracing::warn; - -use crate::slab::Slab; - -#[derive(Clone, Debug)] -pub struct VersionedSlab { - slab: Slab>, - version: NonZeroU32, -} - -#[derive(Clone, Debug)] -struct Slot { - value: T, - version: NonZeroU32, -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct Key { - pub index: u32, - pub version: NonZeroU32, -} - -impl Key { - pub const NULL: Self = Self { - index: u32::MAX, - version: match NonZeroU32::new(u32::MAX) { - Some(n) => n, - None => unreachable!(), - }, - }; - - pub fn new(index: u32, version: NonZeroU32) -> Self { - Self { index, version } - } -} - -impl Default for Key { - fn default() -> Self { - Self::NULL - } -} - -impl Key { - pub fn index(self) -> u32 { - self.index - } - - pub fn version(self) -> NonZeroU32 { - self.version - } -} - -const ONE: NonZeroU32 = match NonZeroU32::new(1) { - Some(n) => n, - None => unreachable!(), -}; - -impl VersionedSlab { - pub const fn new() -> Self { - Self { - slab: Slab::new(), - version: ONE, - } - } - - pub fn get(&self, key: Key) -> Option<&T> { - let slot = self.slab.get(key.index as usize)?; - (slot.version == key.version).then_some(&slot.value) - } - - pub fn get_mut(&mut self, key: Key) -> Option<&mut T> { - let slot = self.slab.get_mut(key.index as usize)?; - (slot.version == key.version).then_some(&mut slot.value) - } - - pub fn len(&self) -> usize { - self.slab.len() - } - - pub fn insert(&mut self, value: T) -> (Key, &mut T) { - self.insert_with(|_| value) - } - - pub fn insert_with(&mut self, f: impl FnOnce(Key) -> T) -> (Key, &mut T) { - let version = self.version; - self.version = NonZeroU32::new(version.get().wrapping_add(1)).unwrap_or_else(|| { - warn!("slab version overflow"); - ONE - }); - - let (index, slot) = self.slab.insert_with(|index| { - assert!( - index < u32::MAX as usize, - "too many values in versioned slab" - ); - Slot { - value: f(Key::new(index as u32, version)), - version, - } - }); - - (Key::new(index as u32, version), &mut slot.value) - } - - pub fn remove(&mut self, key: Key) -> Option { - self.get(key)?; - Some(self.slab.remove(key.index as usize).unwrap().value) - } - - pub fn retain(&mut self, mut f: impl FnMut(Key, &mut T) -> bool) { - self.slab - .retain(|idx, slot| f(Key::new(idx as u32, slot.version), &mut slot.value)) - } - - #[allow(unused)] - pub fn clear(&mut self) { - self.slab.clear(); - } - - pub fn iter(&self) -> impl ExactSizeIterator + FusedIterator + Clone + '_ { - self.slab - .iter() - .map(|(idx, slot)| (Key::new(idx as u32, slot.version), &slot.value)) - } - - pub fn iter_mut( - &mut self, - ) -> impl ExactSizeIterator + FusedIterator + '_ { - self.slab - .iter_mut() - .map(|(idx, slot)| (Key::new(idx as u32, slot.version), &mut slot.value)) - } - - pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ - where - T: Send + Sync, - { - self.slab - .par_iter() - .map(|(idx, slot)| (Key::new(idx as u32, slot.version), &slot.value)) - } - - pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ - where - T: Send + Sync, - { - self.slab - .par_iter_mut() - .map(|(idx, slot)| (Key::new(idx as u32, slot.version), &mut slot.value)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn insert_remove() { - let mut slab = VersionedSlab::new(); - - let k0 = slab.insert(10).0; - let k1 = slab.insert(20).0; - let k2 = slab.insert(30).0; - assert!(k0 != k1 && k1 != k2 && k0 != k2); - - assert_eq!(slab.remove(k1), Some(20)); - assert_eq!(slab.get(k1), None); - assert_eq!(slab.get(k2), Some(&30)); - let k3 = slab.insert(40).0; - assert_eq!(slab.get(k0), Some(&10)); - assert_eq!(slab.get_mut(k3), Some(&mut 40)); - assert_eq!(slab.remove(k0), Some(10)); - - slab.clear(); - assert_eq!(slab.len(), 0); - } - - #[test] - fn retain() { - let mut sm = VersionedSlab::new(); - - let k0 = sm.insert(10).0; - let k1 = sm.insert(20).0; - let k2 = sm.insert(30).0; - - sm.retain(|k, _| k == k1); - - assert_eq!(sm.get(k1), Some(&20)); - assert_eq!(sm.len(), 1); - - assert_eq!(sm.get(k0), None); - assert_eq!(sm.get(k2), None); - } -} diff --git a/crates/valence/src/unit_test/example.rs b/crates/valence/src/unit_test/example.rs new file mode 100644 index 0000000..fedd068 --- /dev/null +++ b/crates/valence/src/unit_test/example.rs @@ -0,0 +1,136 @@ +//! # Unit Test Cookbook +//! +//! Setting up an `App` with a single client: +//! ```ignore +//! # use bevy_app::App; +//! # use valence::unit_test::util::scenario_single_client; +//! let mut app = App::new(); +//! let (client_ent, mut client_helper) = scenario_single_client(&mut app); +//! ``` +//! +//! Asserting packets sent to the client: +//! ```ignore +//! # use bevy_app::App; +//! # use valence::unit_test::util::scenario_single_client; +//! # use valence::client::Client; +//! # fn main() -> anyhow::Result<()> { +//! # let mut app = App::new(); +//! # let (client_ent, mut client_helper) = scenario_single_client(&mut app); +//! # let client: &Client = app.world.get(client_ent).expect("client not found"); +//! client.write_packet(&valence_protocol::packets::s2c::play::KeepAliveS2c { id: 0xdeadbeef }); +//! client.write_packet(&valence_protocol::packets::s2c::play::KeepAliveS2c { id: 0xf00dcafe }); +//! +//! let sent_packets = client_helper.collect_sent()?; +//! assert_packet_count!(sent_packets, 2, S2cPlayPacket::KeepAliveS2c(_)); +//! assert_packet_order!( +//! sent_packets, +//! S2cPlayPacket::KeepAliveS2c(KeepAliveS2c { id: 0xdeadbeef }), +//! S2cPlayPacket::KeepAliveS2c(KeepAliveS2c { id: 0xf00dcafe }), +//! ); +//! # Ok(()) +//! # } +//! ``` +//! +//! Performing a Query without a system is possible, like so: +//! ``` +//! # use bevy_app::App; +//! # use valence::instance::Instance; +//! # let mut app = App::new(); +//! app.world.query::<&Instance>(); +//! ``` + +use bevy_app::App; + +use crate::config::ServerPlugin; +use crate::server::Server; +use crate::unit_test::util::scenario_single_client; + +/// Examples of valence unit tests that need to test the behavior of the server, +/// and not just the logic of a single function. This module is meant to be a +/// pallette of examples for how to write such tests, with various levels of +/// complexity. +/// +/// Some of the tests in this file may be inferior duplicates of real tests. +#[cfg(test)] +mod tests { + use valence_protocol::packets::S2cPlayPacket; + + use super::*; + use crate::client::Client; + use crate::inventory::{Inventory, InventoryKind, OpenInventory}; + use crate::{assert_packet_count, assert_packet_order}; + + /// The server's tick should increment every update. + #[test] + fn example_test_server_tick_increment() { + let mut app = App::new(); + app.add_plugin(ServerPlugin::new(())); + let server = app.world.resource::(); + let tick = server.current_tick(); + app.update(); + let server = app.world.resource::(); + assert_eq!(server.current_tick(), tick + 1); + } + + /// A unit test where we want to test what happens when a client sends a + /// packet to the server. + #[test] + fn example_test_client_position() { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + + // Send a packet as the client to the server. + let packet = valence_protocol::packets::c2s::play::SetPlayerPosition { + position: [12.0, 64.0, 0.0], + on_ground: true, + }; + client_helper.send(&packet); + + // Process the packet. + app.update(); + + // Make assertions + let client: &Client = app.world.get(client_ent).expect("client not found"); + assert_eq!(client.position(), [12.0, 64.0, 0.0].into()); + } + + /// A unit test where we want to test what packets are sent to the client. + #[test] + fn example_test_open_inventory() -> anyhow::Result<()> { + let mut app = App::new(); + let (client_ent, mut client_helper) = scenario_single_client(&mut app); + + let inventory = Inventory::new(InventoryKind::Generic3x3); + let inventory_ent = app.world.spawn(inventory).id(); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Open the inventory. + let open_inventory = OpenInventory::new(inventory_ent); + app.world + .get_entity_mut(client_ent) + .expect("could not find client") + .insert(open_inventory); + + app.update(); + app.update(); + + // Make assertions + app.world + .get::(client_ent) + .expect("client not found"); + let sent_packets = client_helper.collect_sent()?; + + assert_packet_count!(sent_packets, 1, S2cPlayPacket::OpenScreen(_)); + assert_packet_count!(sent_packets, 1, S2cPlayPacket::SetContainerContent(_)); + assert_packet_order!( + sent_packets, + S2cPlayPacket::OpenScreen(_), + S2cPlayPacket::SetContainerContent(_) + ); + + Ok(()) + } +} diff --git a/crates/valence/src/unit_test/mod.rs b/crates/valence/src/unit_test/mod.rs new file mode 100644 index 0000000..6476440 --- /dev/null +++ b/crates/valence/src/unit_test/mod.rs @@ -0,0 +1,2 @@ +mod example; +pub(crate) mod util; diff --git a/crates/valence/src/unit_test/util.rs b/crates/valence/src/unit_test/util.rs new file mode 100644 index 0000000..dadbd2a --- /dev/null +++ b/crates/valence/src/unit_test/util.rs @@ -0,0 +1,204 @@ +use std::sync::{Arc, Mutex}; + +use bevy_app::App; +use bevy_ecs::prelude::Entity; +use bytes::BytesMut; +use valence_protocol::packets::S2cPlayPacket; +use valence_protocol::{EncodePacket, PacketDecoder, PacketEncoder, Username}; + +use crate::client::{Client, ClientConnection}; +use crate::config::{ConnectionMode, ServerPlugin}; +use crate::dimension::DimensionId; +use crate::inventory::{Inventory, InventoryKind}; +use crate::server::{NewClientInfo, Server}; + +/// Creates a mock client that can be used for unit testing. +/// +/// Returns the client, and a helper to inject packets as if the client sent +/// them and receive packets as if the client received them. +pub fn create_mock_client(client_info: NewClientInfo) -> (Client, MockClientHelper) { + let mock_connection = MockClientConnection::new(); + let enc = PacketEncoder::new(); + let dec = PacketDecoder::new(); + let client = Client::new(client_info, Box::new(mock_connection.clone()), enc, dec); + (client, MockClientHelper::new(mock_connection)) +} + +/// Creates a `NewClientInfo` with the given username and a random UUID. +/// Panics if the username is invalid. +pub fn gen_client_info(username: &str) -> NewClientInfo { + NewClientInfo { + username: Username::new(username.to_owned()).unwrap(), + uuid: uuid::Uuid::new_v4(), + ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), + properties: vec![], + } +} + +/// A mock client connection that can be used for testing. +/// +/// Safe to clone, but note that the clone will share the same buffers. +#[derive(Clone)] +pub(crate) struct MockClientConnection { + buffers: Arc>, +} + +struct MockClientBuffers { + /// The queue of packets to receive from the client to be processed by the + /// server. + recv_buf: BytesMut, + /// The queue of packets to send from the server to the client. + send_buf: BytesMut, +} + +impl MockClientConnection { + pub fn new() -> Self { + Self { + buffers: Arc::new(Mutex::new(MockClientBuffers { + recv_buf: BytesMut::new(), + send_buf: BytesMut::new(), + })), + } + } + + pub fn inject_recv(&mut self, bytes: BytesMut) { + self.buffers.lock().unwrap().recv_buf.unsplit(bytes); + } + + pub fn take_sent(&mut self) -> BytesMut { + self.buffers.lock().unwrap().send_buf.split() + } + + pub fn clear_sent(&mut self) { + self.buffers.lock().unwrap().send_buf.clear(); + } +} + +impl ClientConnection for MockClientConnection { + fn try_send(&mut self, bytes: BytesMut) -> anyhow::Result<()> { + self.buffers.lock().unwrap().send_buf.unsplit(bytes); + Ok(()) + } + + fn try_recv(&mut self) -> anyhow::Result { + Ok(self.buffers.lock().unwrap().recv_buf.split()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_client_recv() -> anyhow::Result<()> { + let msg = 0xdeadbeefu32.to_be_bytes(); + let b = BytesMut::from(&msg[..]); + let mut client = MockClientConnection::new(); + client.inject_recv(b); + let b = client.try_recv()?; + assert_eq!(b, BytesMut::from(&msg[..])); + + Ok(()) + } + + #[test] + fn test_mock_client_send() -> anyhow::Result<()> { + let msg = 0xdeadbeefu32.to_be_bytes(); + let b = BytesMut::from(&msg[..]); + let mut client = MockClientConnection::new(); + client.try_send(b)?; + let b = client.take_sent(); + assert_eq!(b, BytesMut::from(&msg[..])); + + Ok(()) + } +} + +/// Contains the mocked client connection and helper methods to inject packets +/// and read packets from the send stream. +pub struct MockClientHelper { + conn: MockClientConnection, + enc: PacketEncoder, + dec: PacketDecoder, +} + +impl MockClientHelper { + fn new(conn: MockClientConnection) -> Self { + Self { + conn, + enc: PacketEncoder::new(), + dec: PacketDecoder::new(), + } + } + + /// Inject a packet to be treated as a packet inbound to the server. Panics + /// if the packet cannot be sent. + pub fn send(&mut self, packet: &impl EncodePacket) { + self.enc + .append_packet(packet) + .expect("failed to encode packet"); + self.conn.inject_recv(self.enc.take()); + } + + /// Collect all packets that have been sent to the client. + pub fn collect_sent<'a>(&'a mut self) -> anyhow::Result>> { + self.dec.queue_bytes(self.conn.take_sent()); + + self.dec.collect_into_vec::>() + } + + pub fn clear_sent(&mut self) { + self.conn.clear_sent(); + } +} + +/// Sets up valence with a single mock client. Returns the Entity of the client +/// and the corresponding MockClientHelper. +/// +/// Reduces boilerplate in unit tests. +pub fn scenario_single_client(app: &mut App) -> (Entity, MockClientHelper) { + app.add_plugin( + ServerPlugin::new(()) + .with_compression_threshold(None) + .with_connection_mode(ConnectionMode::Offline), + ); + let server = app.world.resource::(); + let instance = server.new_instance(DimensionId::default()); + let instance_ent = app.world.spawn(instance).id(); + let info = gen_client_info("test"); + let (mut client, client_helper) = create_mock_client(info); + // HACK: needed so client does not get disconnected on first update + client.set_instance(instance_ent); + let client_ent = app + .world + .spawn((client, Inventory::new(InventoryKind::Player))) + .id(); + (client_ent, client_helper) +} + +#[macro_export] +macro_rules! assert_packet_order { + ($sent_packets:ident, $($packets:pat),+) => {{ + let sent_packets: &Vec = &$sent_packets; + let positions = [ + $((sent_packets.iter().position(|p| matches!(p, $packets))),)* + ]; + assert!(positions.windows(2).all(|w: &[Option]| w[0] < w[1])); + }}; +} + +#[macro_export] +macro_rules! assert_packet_count { + ($sent_packets:ident, $count:tt, $packet:pat) => {{ + let sent_packets: &Vec = &$sent_packets; + let count = sent_packets.iter().filter(|p| matches!(p, $packet)).count(); + assert_eq!( + count, + $count, + "expected {} {} packets, got {}", + $count, + stringify!($packet), + count + ); + }}; +} diff --git a/crates/valence/src/util.rs b/crates/valence/src/util.rs deleted file mode 100644 index 0c1ba3d..0000000 --- a/crates/valence/src/util.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Miscellaneous utilities. - -use num::cast::AsPrimitive; -use num::Float; -use vek::{Aabb, Vec3}; - -pub(crate) fn aabb_from_bottom_and_size(bottom: Vec3, size: Vec3) -> Aabb -where - T: Float + 'static, - f64: AsPrimitive, -{ - let aabb = Aabb { - min: Vec3::new( - bottom.x - size.x / 2.0.as_(), - bottom.y, - bottom.z - size.z / 2.0.as_(), - ), - max: Vec3::new( - bottom.x + size.x / 2.0.as_(), - bottom.y + size.y, - bottom.z + size.z / 2.0.as_(), - ), - }; - - debug_assert!(aabb.is_valid()); - - aabb -} - -/// Takes a normalized direction vector and returns a `(yaw, pitch)` tuple in -/// degrees. -/// -/// This function is the inverse of [`from_yaw_and_pitch`] except for the case -/// where the direction is straight up or down. -pub fn to_yaw_and_pitch(d: Vec3) -> (f64, f64) { - debug_assert!(d.is_normalized(), "the given vector should be normalized"); - - let yaw = f64::atan2(d.z, d.x).to_degrees() - 90.0; - let pitch = -(d.y).asin().to_degrees(); - (yaw, pitch) -} - -/// Takes yaw and pitch angles (in degrees) and returns a normalized -/// direction vector. -/// -/// This function is the inverse of [`to_yaw_and_pitch`]. -pub fn from_yaw_and_pitch(yaw: f64, pitch: f64) -> Vec3 { - let yaw = (yaw + 90.0).to_radians(); - let pitch = (-pitch).to_radians(); - - let xz_len = pitch.cos(); - Vec3::new(yaw.cos() * xz_len, pitch.sin(), yaw.sin() * xz_len) -} - -/// Calculates the minimum number of bits needed to represent the integer `n`. -/// Also known as `floor(log2(n)) + 1`. -/// -/// This returns `0` if `n` is `0`. -pub(crate) const fn bit_width(n: usize) -> usize { - (usize::BITS - n.leading_zeros()) as _ -} - -#[cfg(test)] -mod tests { - use approx::assert_relative_eq; - use rand::random; - - use super::*; - - #[test] - fn yaw_pitch_round_trip() { - for _ in 0..=100 { - let d = (Vec3::new(random(), random(), random()) * 2.0 - 1.0).normalized(); - - let (yaw, pitch) = to_yaw_and_pitch(d); - let d_new = from_yaw_and_pitch(yaw, pitch); - - assert_relative_eq!(d, d_new, epsilon = f64::EPSILON * 100.0); - } - } -} diff --git a/crates/valence/src/view.rs b/crates/valence/src/view.rs new file mode 100644 index 0000000..85b024f --- /dev/null +++ b/crates/valence/src/view.rs @@ -0,0 +1,193 @@ +use glam::DVec3; +use valence_protocol::BlockPos; + +/// The X and Z position of a chunk in an +/// [`Instance`](crate::instance::Instance). +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Debug)] +pub struct ChunkPos { + /// The X position of the chunk. + pub x: i32, + /// The Z position of the chunk. + pub z: i32, +} + +const EXTRA_VIEW_RADIUS: i32 = 2; + +impl ChunkPos { + /// Constructs a new chunk position. + pub const fn new(x: i32, z: i32) -> Self { + Self { x, z } + } + + /// Constructs a chunk position from a position in world space. Only the `x` + /// and `z` components are used. + pub fn from_dvec3(pos: DVec3) -> Self { + Self::at(pos.x, pos.z) + } + + pub fn from_block_pos(pos: BlockPos) -> Self { + Self::new(pos.x.div_euclid(16), pos.z.div_euclid(16)) + } + + /// Takes an X and Z position in world space and returns the chunk position + /// containing the point. + pub fn at(x: f64, z: f64) -> Self { + Self::new((x / 16.0).floor() as i32, (z / 16.0).floor() as i32) + } + + pub fn distance_squared(self, other: Self) -> u64 { + let diff_x = other.x as i64 - self.x as i64; + let diff_z = other.z as i64 - self.z as i64; + + (diff_x * diff_x + diff_z * diff_z) as u64 + } +} + +impl From<(i32, i32)> for ChunkPos { + fn from((x, z): (i32, i32)) -> Self { + Self { x, z } + } +} + +impl From for (i32, i32) { + fn from(pos: ChunkPos) -> Self { + (pos.x, pos.z) + } +} + +impl From<[i32; 2]> for ChunkPos { + fn from([x, z]: [i32; 2]) -> Self { + Self { x, z } + } +} + +impl From for [i32; 2] { + fn from(pos: ChunkPos) -> Self { + [pos.x, pos.z] + } +} + +/// Represents the set of all chunk positions that a client can see, defined by +/// a center chunk position `pos` and view distance `dist`. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct ChunkView { + pub pos: ChunkPos, + pub dist: u8, +} + +impl ChunkView { + #[inline] + pub fn new(pos: impl Into, dist: u8) -> Self { + Self { + pos: pos.into(), + dist, + } + } + + #[must_use] + pub fn with_pos(mut self, pos: impl Into) -> Self { + self.pos = pos.into(); + self + } + + #[must_use] + pub fn with_dist(mut self, dist: u8) -> Self { + self.dist = dist; + self + } + + #[inline] + pub fn contains(self, pos: ChunkPos) -> bool { + let true_dist = self.dist as u64 + EXTRA_VIEW_RADIUS as u64; + self.pos.distance_squared(pos) <= true_dist * true_dist + } + + /// Returns an iterator over all the chunk positions in this view. + pub fn iter(self) -> impl Iterator { + let true_dist = self.dist as i32 + EXTRA_VIEW_RADIUS; + + (self.pos.z - true_dist..=self.pos.z + true_dist) + .flat_map(move |z| { + (self.pos.x - true_dist..=self.pos.x + true_dist).map(move |x| ChunkPos { x, z }) + }) + .filter(move |&p| self.contains(p)) + } + + pub fn diff(self, other: Self) -> impl Iterator { + self.iter().filter(move |&p| !other.contains(p)) + } + + // The foreach-based methods are optimizing better than the iterator ones. + + #[inline] + pub(crate) fn for_each(self, mut f: impl FnMut(ChunkPos)) { + let true_dist = self.dist as i32 + EXTRA_VIEW_RADIUS; + + for z in self.pos.z - true_dist..=self.pos.z + true_dist { + for x in self.pos.x - true_dist..=self.pos.x + true_dist { + let p = ChunkPos { x, z }; + if self.contains(p) { + f(p); + } + } + } + } + + #[inline] + pub(crate) fn diff_for_each(self, other: Self, mut f: impl FnMut(ChunkPos)) { + self.for_each(|p| { + if !other.contains(p) { + f(p); + } + }) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use super::*; + + #[test] + fn chunk_view_for_each_and_iter() { + let pos = ChunkPos::new(42, 24); + + for dist in 2..=32 { + let mut positions = vec![]; + + let view = ChunkView { pos, dist }; + + view.for_each(|pos| { + positions.push(pos); + assert!(view.contains(pos)) + }); + + for (i, pos) in view.iter().enumerate() { + assert_eq!(positions[i], pos); + assert!(view.contains(pos)); + } + } + } + + #[test] + fn chunk_view_contains() { + let view = ChunkView::new([0, 0], 16); + let positions = BTreeSet::from_iter(view.iter()); + + for z in -64..64 { + for x in -64..64 { + let p = ChunkPos::new(x, z); + assert_eq!(view.contains(p), positions.contains(&p)); + } + } + } + + #[test] + fn chunk_pos_round_trip_conv() { + let p = ChunkPos::new(rand::random(), rand::random()); + + assert_eq!(ChunkPos::from(<(i32, i32)>::from(p)), p); + assert_eq!(ChunkPos::from(<[i32; 2]>::from(p)), p); + } +} diff --git a/crates/valence/src/world.rs b/crates/valence/src/world.rs deleted file mode 100644 index 35c73b8..0000000 --- a/crates/valence/src/world.rs +++ /dev/null @@ -1,200 +0,0 @@ -//! A space on a server for objects to occupy. - -use std::iter::FusedIterator; -use std::ops::{Deref, DerefMut, Index, IndexMut}; - -use rayon::iter::ParallelIterator; - -use crate::chunk::Chunks; -use crate::config::Config; -use crate::dimension::DimensionId; -use crate::server::SharedServer; -use crate::slab_versioned::{Key, VersionedSlab}; - -/// A container for all [`World`]s on a [`Server`](crate::server::Server). -pub struct Worlds { - slab: VersionedSlab>, - shared: SharedServer, -} - -/// An identifier for a [`World`] on the server. -/// -/// World IDs are either _valid_ or _invalid_. Valid world IDs point to -/// worlds that have not been deleted, while invalid IDs point to those that -/// have. Once an ID becomes invalid, it will never become valid again. -/// -/// The [`Ord`] instance on this type is correct but otherwise unspecified. This -/// is useful for storing IDs in containers such as -/// [`BTreeMap`](std::collections::BTreeMap). -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub struct WorldId(Key); - -impl WorldId { - /// The value of the default world ID which is always invalid. - pub const NULL: Self = Self(Key::NULL); -} - -impl Worlds { - pub(crate) fn new(shared: SharedServer) -> Self { - Self { - slab: VersionedSlab::new(), - shared, - } - } - - /// Creates a new world on the server with the provided dimension. A - /// reference to the world along with its ID is returned. - pub fn insert( - &mut self, - dimension: DimensionId, - state: C::WorldState, - ) -> (WorldId, &mut World) { - let dim = self.shared.dimension(dimension); - - let (id, world) = self.slab.insert(World { - state, - chunks: Chunks::new( - dim.height, - dim.min_y, - self.shared.biomes().len(), - self.shared.compression_threshold(), - ), - dimension, - deleted: false, - }); - - (WorldId(id), world) - } - - /// Deletes a world from the server. - /// - /// Note that any entities located in the world are not deleted. - /// Additionally, clients that are still in the deleted world at the end - /// of the tick are disconnected. - pub fn remove(&mut self, world: WorldId) -> Option { - self.slab.remove(world.0).map(|w| w.state) - } - - /// Removes all worlds from the server for which `f` returns `false`. - /// - /// All worlds are visited in an unspecified order. - pub fn retain(&mut self, mut f: impl FnMut(WorldId, &mut World) -> bool) { - self.slab.retain(|k, v| f(WorldId(k), v)) - } - - /// Returns the number of worlds on the server. - pub fn len(&self) -> usize { - self.slab.len() - } - - /// Returns `true` if there are no worlds on the server. - pub fn is_empty(&self) -> bool { - self.slab.len() == 0 - } - - /// Returns a shared reference to the world with the given ID. If - /// the ID is invalid, then `None` is returned. - pub fn get(&self, world: WorldId) -> Option<&World> { - self.slab.get(world.0) - } - - /// Returns an exclusive reference to the world with the given ID. If the - /// ID is invalid, then `None` is returned. - pub fn get_mut(&mut self, world: WorldId) -> Option<&mut World> { - self.slab.get_mut(world.0) - } - - /// Returns an iterator over all worlds on the server in an unspecified - /// order. - pub fn iter( - &self, - ) -> impl ExactSizeIterator)> + FusedIterator + Clone + '_ { - self.slab.iter().map(|(k, v)| (WorldId(k), v)) - } - - /// Returns a mutable iterator over all worlds on the server in an - /// unspecified order. - pub fn iter_mut( - &mut self, - ) -> impl ExactSizeIterator)> + FusedIterator + '_ { - self.slab.iter_mut().map(|(k, v)| (WorldId(k), v)) - } - - /// Returns a parallel iterator over all worlds on the server in an - /// unspecified order. - pub fn par_iter(&self) -> impl ParallelIterator)> + Clone + '_ { - self.slab.par_iter().map(|(k, v)| (WorldId(k), v)) - } - - /// Returns a parallel mutable iterator over all worlds on the server in an - /// unspecified order. - pub fn par_iter_mut(&mut self) -> impl ParallelIterator)> + '_ { - self.slab.par_iter_mut().map(|(k, v)| (WorldId(k), v)) - } - - pub(crate) fn update(&mut self) { - self.slab.retain(|_, world| !world.deleted); - - self.par_iter_mut().for_each(|(_, world)| { - world.chunks.update(); - }); - } -} - -impl Index for Worlds { - type Output = World; - - fn index(&self, index: WorldId) -> &Self::Output { - self.get(index).expect("invalid world ID") - } -} - -impl IndexMut for Worlds { - fn index_mut(&mut self, index: WorldId) -> &mut Self::Output { - self.get_mut(index).expect("invalid world ID") - } -} - -/// A space for chunks, entities, and clients to occupy. -pub struct World { - /// Custom state. - pub state: C::WorldState, - pub chunks: Chunks, - dimension: DimensionId, - deleted: bool, -} - -impl Deref for World { - type Target = C::WorldState; - - fn deref(&self) -> &Self::Target { - &self.state - } -} - -impl DerefMut for World { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.state - } -} - -impl World { - /// Gets the dimension the world was created with. - pub fn dimension(&self) -> DimensionId { - self.dimension - } - - pub fn deleted(&self) -> bool { - self.deleted - } - - /// Whether or not this world should be marked as deleted. Deleted worlds - /// are removed from the server at the end of the tick. - /// - /// Note that any entities located in the world are not deleted and their - /// location will not change. Additionally, clients that are still in - /// the deleted world at the end of the tick are disconnected. - pub fn set_deleted(&mut self, deleted: bool) { - self.deleted = deleted; - } -} diff --git a/crates/valence_anvil/Cargo.toml b/crates/valence_anvil/Cargo.toml index f3cfa1f..0d8e040 100644 --- a/crates/valence_anvil/Cargo.toml +++ b/crates/valence_anvil/Cargo.toml @@ -2,7 +2,7 @@ name = "valence_anvil" description = "A library for Minecraft's Anvil world format." documentation = "https://docs.rs/valence_anvil/" -repository = "https://github.com/valence_anvil/valence/tree/main/valence_anvil" +repository = "https://github.com/valence-rs/valence/tree/main/crates/valence_anvil" readme = "README.md" license = "MIT" keywords = ["anvil", "minecraft", "deserialization"] @@ -20,11 +20,14 @@ valence_nbt = { version = "0.5.0", path = "../valence_nbt" } [dev-dependencies] anyhow = "1.0.68" +bevy_ecs = "0.9.1" +clap = "4.1.4" criterion = "0.4.0" +flume = "0.10.14" fs_extra = "1.2.0" tempfile = "3.3.0" -valence = { version = "0.2.0", path = "../valence" } -valence_anvil = { version = "0.1.0", path = ".", features = ["valence"] } +tracing = "0.1.37" +tracing-subscriber = "0.3.16" zip = "0.6.3" [dev-dependencies.reqwest] @@ -36,3 +39,6 @@ features = ["rustls-tls", "blocking", "stream"] [[bench]] name = "world_parsing" harness = false + +[features] +default = ["valence"] diff --git a/crates/valence_anvil/benches/world_parsing.rs b/crates/valence_anvil/benches/world_parsing.rs index b586d05..5cadf1a 100644 --- a/crates/valence_anvil/benches/world_parsing.rs +++ b/crates/valence_anvil/benches/world_parsing.rs @@ -5,7 +5,7 @@ use anyhow::{ensure, Context}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use fs_extra::dir::CopyOptions; use reqwest::IntoUrl; -use valence::chunk::UnloadedChunk; +use valence::instance::Chunk; use valence_anvil::AnvilWorld; use zip::ZipArchive; @@ -33,7 +33,7 @@ fn criterion_benchmark(c: &mut Criterion) { .expect("missing chunk at position") .data; - let mut chunk = UnloadedChunk::new(24); + let mut chunk = Chunk::new(24); valence_anvil::to_valence(&nbt, &mut chunk, 4, |_| Default::default()).unwrap(); diff --git a/crates/valence_anvil/examples/anvil_loading.rs b/crates/valence_anvil/examples/anvil_loading.rs new file mode 100644 index 0000000..322b809 --- /dev/null +++ b/crates/valence_anvil/examples/anvil_loading.rs @@ -0,0 +1,208 @@ +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::path::PathBuf; +use std::thread; + +use clap::Parser; +use flume::{Receiver, Sender}; +use tracing::warn; +use valence::bevy_app::AppExit; +use valence::client::despawn_disconnected_clients; +use valence::client::event::default_event_handler; +use valence::prelude::*; +use valence_anvil::{AnvilChunk, AnvilWorld}; + +const SPAWN_POS: DVec3 = DVec3::new(0.0, 256.0, 0.0); +const SECTION_COUNT: usize = 24; + +#[derive(Parser)] +#[clap(author, version, about)] +struct Cli { + /// The path to a Minecraft world save containing a `region` subdirectory. + path: PathBuf, +} + +#[derive(Resource)] +struct GameState { + /// Chunks that need to be generated. Chunks without a priority have already + /// been sent to the anvil thread. + pending: HashMap>, + sender: Sender, + receiver: Receiver<(ChunkPos, Chunk)>, +} + +/// The order in which chunks should be processed by anvil worker. Smaller +/// values are sent first. +type Priority = u64; + +pub fn main() { + tracing_subscriber::fmt().init(); + + App::new() + .add_plugin(ServerPlugin::new(())) + .add_system_to_stage(EventLoop, default_event_handler) + .add_system_set(PlayerList::default_system_set()) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(remove_unviewed_chunks.after(init_clients)) + .add_system(update_client_views.after(remove_unviewed_chunks)) + .add_system(send_recv_chunks.after(update_client_views)) + .add_system(despawn_disconnected_clients) + .run(); +} + +fn setup(world: &mut World) { + let cli = Cli::parse(); + let dir = cli.path; + + if !dir.exists() { + eprintln!("Directory `{}` does not exist. Exiting.", dir.display()); + world.send_event(AppExit); + } else if !dir.is_dir() { + eprintln!("`{}` is not a directory. Exiting.", dir.display()); + world.send_event(AppExit); + } + + let anvil = AnvilWorld::new(dir); + + let (finished_sender, finished_receiver) = flume::unbounded(); + let (pending_sender, pending_receiver) = flume::unbounded(); + + // Process anvil chunks in a different thread to avoid blocking the main tick + // loop. + thread::spawn(move || anvil_worker(pending_receiver, finished_sender, anvil)); + + world.insert_resource(GameState { + pending: HashMap::new(), + sender: pending_sender, + receiver: finished_receiver, + }); + + let instance = world + .resource::() + .new_instance(DimensionId::default()); + + world.spawn(instance); +} + +fn init_clients( + mut clients: Query<&mut Client, Added>, + instances: Query>, + mut commands: Commands, +) { + for mut client in &mut clients { + let instance = instances.single(); + + client.set_flat(true); + client.set_game_mode(GameMode::Creative); + client.set_position(SPAWN_POS); + client.set_instance(instance); + + commands.spawn(McEntity::with_uuid( + EntityKind::Player, + instance, + client.uuid(), + )); + } +} + +fn remove_unviewed_chunks(mut instances: Query<&mut Instance>) { + instances + .single_mut() + .retain_chunks(|_, chunk| chunk.is_viewed_mut()); +} + +fn update_client_views( + mut instances: Query<&mut Instance>, + mut clients: Query<&mut Client>, + mut state: ResMut, +) { + let instance = instances.single_mut(); + + for client in &mut clients { + let view = client.view(); + let queue_pos = |pos| { + if instance.chunk(pos).is_none() { + match state.pending.entry(pos) { + Entry::Occupied(mut oe) => { + if let Some(priority) = oe.get_mut() { + let dist = view.pos.distance_squared(pos); + *priority = (*priority).min(dist); + } + } + Entry::Vacant(ve) => { + let dist = view.pos.distance_squared(pos); + ve.insert(Some(dist)); + } + } + } + }; + + // Queue all the new chunks in the view to be sent to the anvil worker. + if client.is_added() { + view.iter().for_each(queue_pos); + } else { + let old_view = client.old_view(); + if old_view != view { + view.diff(old_view).for_each(queue_pos); + } + } + } +} + +fn send_recv_chunks(mut instances: Query<&mut Instance>, state: ResMut) { + let mut instance = instances.single_mut(); + let state = state.into_inner(); + + // Insert the chunks that are finished loading into the instance. + for (pos, chunk) in state.receiver.drain() { + instance.insert_chunk(pos, chunk); + assert!(state.pending.remove(&pos).is_some()); + } + + // Collect all the new chunks that need to be loaded this tick. + let mut to_send = vec![]; + + for (pos, priority) in &mut state.pending { + if let Some(pri) = priority.take() { + to_send.push((pri, pos)); + } + } + + // Sort chunks by ascending priority. + to_send.sort_unstable_by_key(|(pri, _)| *pri); + + // Send the sorted chunks to be loaded. + for (_, pos) in to_send { + let _ = state.sender.try_send(*pos); + } +} + +fn anvil_worker( + receiver: Receiver, + sender: Sender<(ChunkPos, Chunk)>, + mut world: AnvilWorld, +) { + while let Ok(pos) = receiver.recv() { + match get_chunk(pos, &mut world) { + Ok(chunk) => { + if let Some(chunk) = chunk { + let _ = sender.try_send((pos, chunk)); + } + } + Err(e) => warn!("Failed to get chunk at ({}, {}): {e:#}.", pos.x, pos.z), + } + } +} + +fn get_chunk(pos: ChunkPos, world: &mut AnvilWorld) -> anyhow::Result> { + let Some(AnvilChunk { data, .. }) = world.read_chunk(pos.x, pos.z)? else { + return Ok(None) + }; + + let mut chunk = Chunk::new(SECTION_COUNT); + + valence_anvil::to_valence(&data, &mut chunk, 4, |_| BiomeId::default())?; + + Ok(Some(chunk)) +} diff --git a/crates/valence_anvil/examples/valence_loading.rs b/crates/valence_anvil/examples/valence_loading.rs deleted file mode 100644 index 0de7135..0000000 --- a/crates/valence_anvil/examples/valence_loading.rs +++ /dev/null @@ -1,202 +0,0 @@ -//! # IMPORTANT -//! -//! Run this example with one argument containing the path of the the following -//! to the world directory you wish to load. Inside this directory you can -//! commonly see `advancements`, `DIM1`, `DIM-1` and most importantly `region` -//! subdirectories. Only the `region` directory is accessed. - -extern crate valence; - -use std::env; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use valence::prelude::*; -use valence_anvil::AnvilWorld; - -pub fn main() -> ShutdownResult { - let Some(world_dir) = env::args().nth(1) else { - return Err("please add the world directory as program argument.".into()) - }; - - let world_dir = PathBuf::from(world_dir); - - if !world_dir.exists() || !world_dir.is_dir() { - return Err("world argument must be a directory that exists".into()); - } - - if !world_dir.join("region").exists() { - return Err("could not find the \"region\" directory in the given world directory".into()); - } - - valence::start_server( - Game { - world_dir, - player_count: AtomicUsize::new(0), - }, - None, - ) -} - -#[derive(Debug, Default)] -struct ClientData { - id: EntityId, - //block: valence::block::BlockKind -} - -struct Game { - world_dir: PathBuf, - player_count: AtomicUsize, -} - -const MAX_PLAYERS: usize = 10; - -#[async_trait] -impl Config for Game { - type ServerState = Option; - type ClientState = ClientData; - type EntityState = (); - type WorldState = AnvilWorld; - /// If the chunk should stay loaded at the end of the tick. - type ChunkState = bool; - type PlayerListState = (); - type InventoryState = (); - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - _protocol_version: i32, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: self.player_count.load(Ordering::SeqCst) as i32, - max_players: MAX_PLAYERS as i32, - player_sample: Default::default(), - description: "Hello Valence!".color(Color::AQUA), - favicon_png: Some( - include_bytes!("../../../assets/logo-64x64.png") - .as_slice() - .into(), - ), - } - } - - fn init(&self, server: &mut Server) { - for (id, _) in server.shared.dimensions() { - server.worlds.insert(id, AnvilWorld::new(&self.world_dir)); - } - server.state = Some(server.player_lists.insert(()).0); - } - - fn update(&self, server: &mut Server) { - let (world_id, world) = server.worlds.iter_mut().next().unwrap(); - - server.clients.retain(|_, client| { - if client.created_this_tick() { - if self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then_some(count + 1) - }) - .is_err() - { - client.disconnect("The server is full!".color(Color::RED)); - return false; - } - - match server - .entities - .insert_with_uuid(EntityKind::Player, client.uuid(), ()) - { - Some((id, _)) => client.state.id = id, - None => { - client.disconnect("Conflicting UUID"); - return false; - } - } - - client.respawn(world_id); - client.set_flat(true); - client.set_game_mode(GameMode::Spectator); - client.teleport([0.0, 125.0, 0.0], 0.0, 0.0); - client.set_player_list(server.state.clone()); - - if let Some(id) = &server.state { - server.player_lists.get_mut(id).insert( - client.uuid(), - client.username(), - client.textures().cloned(), - client.game_mode(), - 0, - None, - true, - ); - } - - client.send_message("Welcome to the java chunk parsing example!"); - client.send_message( - "Chunks with a single lava source block indicates that the chunk is not \ - (fully) generated." - .italic(), - ); - } - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - if let Some(id) = &server.state { - server.player_lists.get_mut(id).remove(client.uuid()); - } - server.entities.delete(client.id); - - return false; - } - - if let Some(entity) = server.entities.get_mut(client.state.id) { - while let Some(event) = client.next_event() { - event.handle_default(client, entity); - } - } - - let dist = client.view_distance(); - let p = client.position(); - - for pos in ChunkPos::at(p.x, p.z).in_view(dist) { - if let Some(existing) = world.chunks.get_mut(pos) { - existing.state = true; - } else { - match world.state.read_chunk(pos.x, pos.z) { - Ok(Some(anvil_chunk)) => { - let mut chunk = UnloadedChunk::new(24); - - if let Err(e) = - valence_anvil::to_valence(&anvil_chunk.data, &mut chunk, 4, |_| { - BiomeId::default() - }) - { - eprintln!("Failed to convert chunk at ({}, {}): {e}", pos.x, pos.z); - } - - world.chunks.insert(pos, chunk, true); - } - Ok(None) => { - // No chunk at this position. - world.chunks.insert(pos, UnloadedChunk::default(), true); - } - Err(e) => { - eprintln!("Failed to read chunk at ({}, {}): {e}", pos.x, pos.z) - } - } - } - } - - true - }); - - for (_, chunk) in world.chunks.iter_mut() { - if !chunk.state { - chunk.set_deleted(true) - } - } - } -} diff --git a/crates/valence_anvil/src/to_valence.rs b/crates/valence_anvil/src/to_valence.rs index 0537cb2..c90f8e3 100644 --- a/crates/valence_anvil/src/to_valence.rs +++ b/crates/valence_anvil/src/to_valence.rs @@ -1,7 +1,7 @@ use num_integer::div_ceil; use thiserror::Error; use valence::biome::BiomeId; -use valence::chunk::Chunk; +use valence::instance::Chunk; use valence::protocol::block::{BlockKind, PropName, PropValue}; use valence::protocol::Ident; use valence_nbt::{Compound, List, Value}; @@ -53,14 +53,14 @@ pub enum ToValenceError { BadBiomePaletteIndex, } -/// Reads an Anvil chunk in NBT form and writes its data to a Valence [`Chunk`]. +/// Takes an Anvil chunk in NBT form and writes its data to a Valence [`Chunk`]. /// An error is returned if the NBT data does not match the expected structure /// for an Anvil chunk. /// /// # Arguments /// /// - `nbt`: The Anvil chunk to read from. This is usually the value returned by -/// [`read_chunk`]. +/// [`AnvilWorld::read_chunk`]. /// - `chunk`: The Valence chunk to write to. /// - `sect_offset`: A constant to add to all sector Y positions in `nbt`. After /// applying the offset, only the sectors in the range @@ -68,15 +68,14 @@ pub enum ToValenceError { /// - `map_biome`: A function to map biome resource identifiers in the NBT data /// to Valence [`BiomeId`]s. /// -/// [`read_chunk`]: crate::AnvilWorld::read_chunk -pub fn to_valence( +/// [`AnvilWorld::read_chunk`]: crate::AnvilWorld::read_chunk +pub fn to_valence( nbt: &Compound, - chunk: &mut C, + chunk: &mut Chunk, sect_offset: i32, mut map_biome: F, ) -> Result<(), ToValenceError> where - C: Chunk, F: FnMut(Ident<&str>) -> BiomeId, { let Some(Value::List(List::Compound(sections))) = nbt.get("sections") else { diff --git a/crates/valence_protocol/Cargo.toml b/crates/valence_protocol/Cargo.toml index 140e933..c5c4b0a 100644 --- a/crates/valence_protocol/Cargo.toml +++ b/crates/valence_protocol/Cargo.toml @@ -17,7 +17,7 @@ serde_json = "1.0.87" thiserror = "1.0.37" tracing = "0.1.37" uuid = { version = "1.2.1", features = ["serde"] } -valence_derive = { version = "0.1.0", path = "../valence_derive" } +valence_protocol_macros = { version = "0.1.0", path = "../valence_protocol_macros" } valence_nbt = { version = "0.5.0", path = "../valence_nbt" } [[bench]] diff --git a/crates/valence_protocol/benches/benches.rs b/crates/valence_protocol/benches/benches.rs index 77ff6e1..0a053f8 100644 --- a/crates/valence_protocol/benches/benches.rs +++ b/crates/valence_protocol/benches/benches.rs @@ -130,12 +130,13 @@ fn packets(c: &mut Criterion) { }; let tab_list_header_footer_packet = SetTabListHeaderAndFooter { - header: "this".italic() + " is the " + "header".bold().color(Color::RED), - footer: "this".italic() + header: ("this".italic() + " is the " + "header".bold().color(Color::RED)).into(), + footer: ("this".italic() + " is the " + "footer".bold().color(Color::BLUE) + ". I am appending some extra text so that the packet goes over the compression \ - threshold.", + threshold.") + .into(), }; let spawn_entity_packet = SpawnEntity { diff --git a/crates/valence_protocol/src/codec.rs b/crates/valence_protocol/src/codec.rs index 2dbb6a2..8c9dbd1 100644 --- a/crates/valence_protocol/src/codec.rs +++ b/crates/valence_protocol/src/codec.rs @@ -1,5 +1,3 @@ -use std::fmt; - #[cfg(feature = "encryption")] use aes::cipher::{AsyncStreamCipher, NewCipher}; use anyhow::{bail, ensure}; @@ -30,6 +28,7 @@ impl PacketEncoder { Self::default() } + #[inline] pub fn append_bytes(&mut self, bytes: &[u8]) { self.buf.extend_from_slice(bytes) } @@ -288,7 +287,7 @@ impl PacketDecoder { pub fn try_next_packet<'a, P>(&'a mut self) -> Result> where - P: DecodePacket<'a> + fmt::Debug, + P: DecodePacket<'a>, { self.buf.advance(self.cursor); self.cursor = 0; @@ -362,6 +361,69 @@ impl PacketDecoder { Ok(Some(packet)) } + /// Repeatedly decodes a packet type until all packets in the decoder are + /// consumed or an error occurs. The decoded packets are returned in a vec. + /// + /// Intended for testing purposes with encryption and compression disabled. + #[track_caller] + pub fn collect_into_vec<'a, P>(&'a mut self) -> Result> + where + P: DecodePacket<'a>, + { + #[cfg(feature = "encryption")] + assert!( + self.cipher.is_none(), + "encryption must be disabled to use this method" + ); + + #[cfg(feature = "compression")] + assert!( + !self.compression_enabled, + "compression must be disabled to use this method" + ); + + self.buf.advance(self.cursor); + self.cursor = 0; + + let mut res = vec![]; + + loop { + let mut r = &self.buf[self.cursor..]; + + let packet_len = match VarInt::decode_partial(&mut r) { + Ok(len) => len, + Err(VarIntDecodeError::Incomplete) => return Ok(res), + Err(VarIntDecodeError::TooLarge) => bail!("malformed packet length VarInt"), + }; + + ensure!( + (0..=MAX_PACKET_SIZE).contains(&packet_len), + "packet length of {packet_len} is out of bounds" + ); + + if r.len() < packet_len as usize { + return Ok(res); + } + + r = &r[..packet_len as usize]; + + let packet = P::decode_packet(&mut r)?; + + if !r.is_empty() { + let remaining = r.len(); + + debug!("packet after partial decode ({remaining} bytes remain): {packet:?}"); + + bail!("packet contents were not read completely ({remaining} bytes remain)"); + } + + let total_packet_len = VarInt(packet_len).written_size() + packet_len as usize; + self.cursor += total_packet_len; + + res.push(packet); + } + } + pub fn has_next_packet(&self) -> Result { let mut r = &self.buf[self.cursor..]; @@ -535,4 +597,25 @@ mod tests { .unwrap() .check("third"); } + + #[test] + fn collect_packets_into_vec() { + let packets = vec![ + TestPacket::new("foo"), + TestPacket::new("bar"), + TestPacket::new("baz"), + ]; + + let mut enc = PacketEncoder::new(); + let mut dec = PacketDecoder::new(); + + for pkt in &packets { + enc.append_packet(pkt).unwrap(); + } + + dec.queue_bytes(enc.take()); + let res = dec.collect_into_vec::().unwrap(); + + assert_eq!(packets, res); + } } diff --git a/crates/valence_protocol/src/impls.rs b/crates/valence_protocol/src/impls.rs index 00d2e13..cfd0d20 100644 --- a/crates/valence_protocol/src/impls.rs +++ b/crates/valence_protocol/src/impls.rs @@ -22,6 +22,7 @@ impl Encode for bool { } fn write_slice(slice: &[bool], mut w: impl Write) -> io::Result<()> { + // Bools are guaranteed to have the correct bit pattern. let bytes: &[u8] = unsafe { mem::transmute(slice) }; w.write_all(bytes) } @@ -32,7 +33,7 @@ impl Encode for bool { impl Decode<'_> for bool { fn decode(r: &mut &[u8]) -> Result { let n = r.read_u8()?; - ensure!(n <= 1, "boolean is not 0 or 1"); + ensure!(n <= 1, "decoded boolean is not 0 or 1"); Ok(n == 1) } } @@ -589,20 +590,20 @@ impl<'a, T: Decode<'a>> Decode<'a> for Option { impl<'a, B> Encode for Cow<'a, B> where - B: ToOwned + Encode, + B: ToOwned + Encode + ?Sized, { fn encode(&self, w: impl Write) -> Result<()> { self.as_ref().encode(w) } } -impl<'a, B> Decode<'a> for Cow<'a, B> +impl<'a, 'b, B> Decode<'a> for Cow<'b, B> where - B: ToOwned, - &'a B: Decode<'a>, + B: ToOwned + ?Sized, + B::Owned: Decode<'a>, { fn decode(r: &mut &'a [u8]) -> Result { - <&B>::decode(r).map(Cow::Borrowed) + B::Owned::decode(r).map(Cow::Owned) } } diff --git a/crates/valence_protocol/src/inventory.rs b/crates/valence_protocol/src/inventory.rs deleted file mode 100644 index 14e8657..0000000 --- a/crates/valence_protocol/src/inventory.rs +++ /dev/null @@ -1,62 +0,0 @@ -use valence_derive::{Decode, Encode}; - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] -pub enum InventoryKind { - Generic9x1, - Generic9x2, - Generic9x3, - Generic9x4, - Generic9x5, - Generic9x6, - Generic3x3, - Anvil, - Beacon, - BlastFurnace, - BrewingStand, - Crafting, - Enchantment, - Furnace, - Grindstone, - Hopper, - Lectern, - Loom, - Merchant, - ShulkerBox, - Smithing, - Smoker, - Cartography, - Stonecutter, -} - -impl InventoryKind { - /// The number of slots in this inventory, not counting the player's main - /// inventory slots. - pub const fn slot_count(self) -> usize { - match self { - InventoryKind::Generic9x1 => 9, - InventoryKind::Generic9x2 => 9 * 2, - InventoryKind::Generic9x3 => 9 * 3, - InventoryKind::Generic9x4 => 9 * 4, - InventoryKind::Generic9x5 => 9 * 5, - InventoryKind::Generic9x6 => 9 * 6, - InventoryKind::Generic3x3 => 3 * 3, - InventoryKind::Anvil => 4, - InventoryKind::Beacon => 1, - InventoryKind::BlastFurnace => 3, - InventoryKind::BrewingStand => 5, - InventoryKind::Crafting => 10, - InventoryKind::Enchantment => 2, - InventoryKind::Furnace => 3, - InventoryKind::Grindstone => 3, - InventoryKind::Hopper => 5, - InventoryKind::Lectern => 1, - InventoryKind::Loom => 4, - InventoryKind::Merchant => 3, - InventoryKind::ShulkerBox => 27, - InventoryKind::Smithing => 3, - InventoryKind::Smoker => 3, - InventoryKind::Cartography => 3, - InventoryKind::Stonecutter => 2, - } - } -} diff --git a/crates/valence_protocol/src/lib.rs b/crates/valence_protocol/src/lib.rs index 63d11e3..0fd78d8 100644 --- a/crates/valence_protocol/src/lib.rs +++ b/crates/valence_protocol/src/lib.rs @@ -68,8 +68,8 @@ // Allows us to use our own proc macros internally. extern crate self as valence_protocol; -use std::io; use std::io::Write; +use std::{fmt, io}; pub use anyhow::{Error, Result}; pub use array::LengthPrefixedArray; @@ -78,13 +78,12 @@ pub use block_pos::BlockPos; pub use byte_angle::ByteAngle; pub use codec::*; pub use ident::Ident; -pub use inventory::InventoryKind; pub use item::{ItemKind, ItemStack}; pub use raw_bytes::RawBytes; pub use text::{Text, TextFormat}; pub use username::Username; pub use uuid::Uuid; -pub use valence_derive::{Decode, DecodePacket, Encode, EncodePacket}; +pub use valence_protocol_macros::{Decode, DecodePacket, Encode, EncodePacket}; pub use var_int::VarInt; pub use var_long::VarLong; pub use {uuid, valence_nbt as nbt}; @@ -106,7 +105,6 @@ pub mod enchant; pub mod entity_meta; pub mod ident; mod impls; -mod inventory; mod item; pub mod packets; mod raw_bytes; @@ -174,7 +172,7 @@ pub const MAX_PACKET_SIZE: i32 = 2097152; /// println!("{buf:?}"); /// ``` /// -/// [macro]: valence_derive::Encode +/// [macro]: valence_protocol_macros::Encode pub trait Encode { /// Writes this object to the provided writer. /// @@ -249,7 +247,7 @@ pub trait Encode { /// assert!(r.is_empty()); /// ``` /// -/// [macro]: valence_derive::Decode +/// [macro]: valence_protocol_macros::Decode pub trait Decode<'a>: Sized { /// Reads this object from the provided byte slice. /// @@ -284,8 +282,8 @@ pub trait Decode<'a>: Sized { /// println!("{buf:?}"); /// ``` /// -/// [macro]: valence_derive::DecodePacket -pub trait EncodePacket { +/// [macro]: valence_protocol_macros::DecodePacket +pub trait EncodePacket: fmt::Debug { /// The packet ID that is written when [`Self::encode_packet`] is called. A /// negative value indicates that the packet ID is not statically known. const PACKET_ID: i32 = -1; @@ -324,7 +322,7 @@ pub trait EncodePacket { /// ``` /// /// [macro]: valence_protocol::DecodePacket -pub trait DecodePacket<'a>: Sized { +pub trait DecodePacket<'a>: Sized + fmt::Debug { /// The packet ID that is read when [`Self::decode_packet`] is called. A /// negative value indicates that the packet ID is not statically known. const PACKET_ID: i32 = -1; @@ -339,7 +337,7 @@ pub trait DecodePacket<'a>: Sized { mod derive_tests { use super::*; - #[derive(Encode, EncodePacket, Decode, DecodePacket)] + #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)] #[packet_id = 1] struct RegularStruct { foo: i32, @@ -347,30 +345,30 @@ mod derive_tests { baz: f64, } - #[derive(Encode, EncodePacket, Decode, DecodePacket)] + #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)] #[packet_id = 2] struct UnitStruct; - #[derive(Encode, EncodePacket, Decode, DecodePacket)] + #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)] #[packet_id = 3] struct EmptyStruct {} - #[derive(Encode, EncodePacket, Decode, DecodePacket)] + #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)] #[packet_id = 4] struct TupleStruct(i32, bool, f64); - #[derive(Encode, EncodePacket, Decode, DecodePacket)] + #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)] #[packet_id = 5] - struct StructWithGenerics<'z, T = ()> { + struct StructWithGenerics<'z, T: std::fmt::Debug = ()> { foo: &'z str, bar: T, } - #[derive(Encode, EncodePacket, Decode, DecodePacket)] + #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)] #[packet_id = 6] - struct TupleStructWithGenerics<'z, T = ()>(&'z str, i32, T); + struct TupleStructWithGenerics<'z, T: std::fmt::Debug = ()>(&'z str, i32, T); - #[derive(Encode, EncodePacket, Decode, DecodePacket)] + #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)] #[packet_id = 7] enum RegularEnum { Empty, @@ -378,13 +376,13 @@ mod derive_tests { Fields { foo: i32, bar: bool, baz: f64 }, } - #[derive(Encode, EncodePacket, Decode, DecodePacket)] + #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)] #[packet_id = 8] enum EmptyEnum {} - #[derive(Encode, EncodePacket, Decode, DecodePacket)] + #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)] #[packet_id = 0xbeef] - enum EnumWithGenericsAndTags<'z, T = ()> { + enum EnumWithGenericsAndTags<'z, T: std::fmt::Debug = ()> { #[tag = 5] First { foo: &'z str, diff --git a/crates/valence_protocol/src/packets/s2c.rs b/crates/valence_protocol/src/packets/s2c.rs index 15dad5a..04aa993 100644 --- a/crates/valence_protocol/src/packets/s2c.rs +++ b/crates/valence_protocol/src/packets/s2c.rs @@ -1,5 +1,6 @@ +use std::borrow::Cow; + use uuid::Uuid; -use valence_derive::{Decode, DecodePacket, Encode, EncodePacket}; use valence_nbt::Compound; use crate::block_pos::BlockPos; @@ -11,14 +12,14 @@ use crate::text::Text; use crate::types::{ AttributeProperty, BossBarAction, ChatSuggestionAction, ChunkDataBlockEntity, CommandSuggestionMatch, Difficulty, EntityEffectFlags, FeetOrEyes, GameEventKind, GameMode, - GlobalPos, Hand, LookAtEntity, MerchantTrade, PlayerAbilitiesFlags, SignedProperty, - SoundCategory, Statistic, SyncPlayerPosLookFlags, TagGroup, UpdateObjectiveMode, - UpdateScoreAction, + GlobalPos, Hand, LookAtEntity, MerchantTrade, PlayerAbilitiesFlags, Property, SoundCategory, + Statistic, SyncPlayerPosLookFlags, TagGroup, UpdateObjectiveMode, UpdateScoreAction, + WindowType, }; use crate::username::Username; use crate::var_int::VarInt; use crate::var_long::VarLong; -use crate::LengthPrefixedArray; +use crate::{Decode, DecodePacket, Encode, EncodePacket, LengthPrefixedArray}; pub mod commands; pub mod declare_recipes; @@ -63,8 +64,8 @@ pub mod login { #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x00] - pub struct DisconnectLogin { - pub reason: Text, + pub struct DisconnectLogin<'a> { + pub reason: Cow<'a, Text>, } #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -80,7 +81,7 @@ pub mod login { pub struct LoginSuccess<'a> { pub uuid: Uuid, pub username: Username<&'a str>, - pub properties: Vec>, + pub properties: Cow<'a, [Property]>, } #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -100,7 +101,7 @@ pub mod login { packet_enum! { #[derive(Clone)] S2cLoginPacket<'a> { - DisconnectLogin, + DisconnectLogin<'a>, EncryptionRequest<'a>, LoginSuccess<'a>, SetCompression, @@ -327,17 +328,17 @@ pub mod play { #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x17] - pub struct DisconnectPlay { - pub reason: Text, + pub struct DisconnectPlay<'a> { + pub reason: Cow<'a, Text>, } #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x18] - pub struct DisguisedChatMessage { - pub message: Text, + pub struct DisguisedChatMessage<'a> { + pub message: Cow<'a, Text>, pub chat_type: VarInt, - pub chat_type_name: Text, - pub target_name: Option, + pub chat_type_name: Cow<'a, Text>, + pub target_name: Option>, } #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -553,10 +554,10 @@ pub mod play { #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x2c] - pub struct OpenScreen { + pub struct OpenScreen<'a> { pub window_id: VarInt, - pub window_type: VarInt, - pub window_title: Text, + pub window_type: WindowType, + pub window_title: Cow<'a, Text>, } #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -601,17 +602,17 @@ pub mod play { #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x34] - pub struct CombatDeath { + pub struct CombatDeath<'a> { pub player_id: VarInt, /// Killer's entity ID, -1 if no killer pub entity_id: i32, - pub message: Text, + pub message: Cow<'a, Text>, } #[derive(Clone, PartialEq, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x35] - pub struct PlayerInfoRemove { - pub players: Vec, + pub struct PlayerInfoRemove<'a> { + pub uuids: Cow<'a, [Uuid]>, } #[derive(Copy, Clone, PartialEq, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -658,7 +659,7 @@ pub mod play { pub url: &'a str, pub hash: &'a str, pub forced: bool, - pub prompt_message: Option, + pub prompt_message: Option>, } #[derive(Clone, PartialEq, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -722,15 +723,15 @@ pub mod play { #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x41] pub struct ServerData<'a> { - pub motd: Option, + pub motd: Option>, pub icon: Option<&'a str>, pub enforce_secure_chat: bool, } #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x42] - pub struct SetActionBarText { - pub action_bar_text: Text, + pub struct SetActionBarText<'a> { + pub action_bar_text: Cow<'a, Text>, } #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -878,8 +879,8 @@ pub mod play { #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x59] - pub struct SetSubtitleText { - pub subtitle_text: Text, + pub struct SetSubtitleText<'a> { + pub subtitle_text: Cow<'a, Text>, } #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -895,8 +896,8 @@ pub mod play { #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x5b] - pub struct SetTitleText { - pub title_text: Text, + pub struct SetTitleText<'a> { + pub title_text: Cow<'a, Text>, } #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -934,17 +935,17 @@ pub mod play { #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x60] - pub struct SystemChatMessage { - pub chat: Text, + pub struct SystemChatMessage<'a> { + pub chat: Cow<'a, Text>, /// Whether the message is in the actionbar or the chat. pub overlay: bool, } #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x61] - pub struct SetTabListHeaderAndFooter { - pub header: Text, - pub footer: Text, + pub struct SetTabListHeaderAndFooter<'a> { + pub header: Cow<'a, Text>, + pub footer: Cow<'a, Text>, } #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -1034,8 +1035,8 @@ pub mod play { ChatSuggestions<'a>, PluginMessageS2c<'a>, DeleteMessage<'a>, - DisconnectPlay, - DisguisedChatMessage, + DisconnectPlay<'a>, + DisguisedChatMessage<'a>, EntityEvent, PlaceRecipe<'a>, UnloadChunk, @@ -1055,7 +1056,7 @@ pub mod play { UpdateEntityRotation, MoveVehicle, OpenBook, - OpenScreen, + OpenScreen<'a>, OpenSignEditor, PingPlay, PlaceGhostRecipe<'a>, @@ -1063,8 +1064,8 @@ pub mod play { PlayerChatMessage<'a>, EndCombat, EnterCombat, - CombatDeath, - PlayerInfoRemove, + CombatDeath<'a>, + PlayerInfoRemove<'a>, PlayerInfoUpdate<'a>, LookAt, SynchronizePlayerPosition, @@ -1077,7 +1078,7 @@ pub mod play { UpdateSectionBlocks, SelectAdvancementsTab<'a>, ServerData<'a>, - SetActionBarText, + SetActionBarText<'a>, SetBorderCenter, SetBorderLerpSize, SetBorderSize, @@ -1100,15 +1101,15 @@ pub mod play { UpdateTeams<'a>, UpdateScore<'a>, SetSimulationDistance, - SetSubtitleText, + SetSubtitleText<'a>, UpdateTime, - SetTitleText, + SetTitleText<'a>, SetTitleAnimationTimes, EntitySoundEffect, SoundEffect<'a>, StopSound<'a>, - SystemChatMessage, - SetTabListHeaderAndFooter, + SystemChatMessage<'a>, + SetTabListHeaderAndFooter<'a>, TagQueryResponse, PickupItem, TeleportEntity, diff --git a/crates/valence_protocol/src/packets/s2c/map_data.rs b/crates/valence_protocol/src/packets/s2c/map_data.rs index 86c62c5..9c7bca0 100644 --- a/crates/valence_protocol/src/packets/s2c/map_data.rs +++ b/crates/valence_protocol/src/packets/s2c/map_data.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::io::Write; use crate::{Decode, DecodePacket, Encode, EncodePacket, Text, VarInt}; @@ -8,18 +9,18 @@ pub struct MapData<'a> { pub map_id: VarInt, pub scale: i8, pub locked: bool, - pub icons: Option>, + pub icons: Option>>, pub data: Option>, } #[derive(Clone, PartialEq, Debug, Encode, Decode)] -pub struct Icon { +pub struct Icon<'a> { pub icon_type: IconType, /// In map coordinates; -128 for furthest left, +127 for furthest right pub position: [i8; 2], /// 0 is a vertical icon and increments by 22.5° pub direction: i8, - pub display_name: Option, + pub display_name: Option>, } #[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] @@ -82,7 +83,7 @@ impl<'a> Decode<'a> for MapData<'a> { let map_id = VarInt::decode(r)?; let scale = i8::decode(r)?; let locked = bool::decode(r)?; - let icons = >>::decode(r)?; + let icons = >>>::decode(r)?; let columns = u8::decode(r)?; let data = if columns > 0 { diff --git a/crates/valence_protocol/src/packets/s2c/player_chat_message.rs b/crates/valence_protocol/src/packets/s2c/player_chat_message.rs index 21e7ddb..ec04054 100644 --- a/crates/valence_protocol/src/packets/s2c/player_chat_message.rs +++ b/crates/valence_protocol/src/packets/s2c/player_chat_message.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::io::Write; use crate::packets::s2c::message_signature::MessageSignature; @@ -13,12 +14,12 @@ pub struct PlayerChatMessage<'a> { pub time_stamp: u64, pub salt: u64, pub previous_messages: Vec>, - pub unsigned_content: Option, + pub unsigned_content: Option>, pub filter_type: MessageFilterType, pub filter_type_bits: Option, pub chat_type: VarInt, - pub network_name: Text, - pub network_target_name: Option, + pub network_name: Cow<'a, Text>, + pub network_target_name: Option>, } #[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] @@ -65,7 +66,7 @@ impl<'a> Decode<'a> for PlayerChatMessage<'a> { let time_stamp = u64::decode(r)?; let salt = u64::decode(r)?; let previous_messages = Vec::::decode(r)?; - let unsigned_content = Option::::decode(r)?; + let unsigned_content = Option::>::decode(r)?; let filter_type = MessageFilterType::decode(r)?; let filter_type_bits = match filter_type { @@ -74,8 +75,8 @@ impl<'a> Decode<'a> for PlayerChatMessage<'a> { }; let chat_type = VarInt::decode(r)?; - let network_name = Text::decode(r)?; - let network_target_name = Option::::decode(r)?; + let network_name = >::decode(r)?; + let network_target_name = Option::>::decode(r)?; Ok(Self { sender, diff --git a/crates/valence_protocol/src/packets/s2c/player_info_update.rs b/crates/valence_protocol/src/packets/s2c/player_info_update.rs index 4933bf0..947e6b1 100644 --- a/crates/valence_protocol/src/packets/s2c/player_info_update.rs +++ b/crates/valence_protocol/src/packets/s2c/player_info_update.rs @@ -1,16 +1,17 @@ +use std::borrow::Cow; use std::io::Write; use bitfield_struct::bitfield; use uuid::Uuid; -use crate::types::{GameMode, SignedProperty}; +use crate::types::{GameMode, Property}; use crate::{Decode, DecodePacket, Encode, EncodePacket, Text, VarInt}; #[derive(Clone, Debug, EncodePacket, DecodePacket)] #[packet_id = 0x36] pub struct PlayerInfoUpdate<'a> { pub actions: Actions, - pub entries: Vec>, + pub entries: Cow<'a, [Entry<'a>]>, } #[bitfield(u8)] @@ -29,12 +30,12 @@ pub struct Actions { pub struct Entry<'a> { pub player_uuid: Uuid, pub username: &'a str, - pub properties: Vec>, + pub properties: Cow<'a, [Property]>, pub chat_data: Option>, pub listed: bool, pub ping: i32, pub game_mode: GameMode, - pub display_name: Option, + pub display_name: Option>, } #[derive(Clone, PartialEq, Debug, Encode, Decode)] @@ -53,7 +54,7 @@ impl<'a> Encode for PlayerInfoUpdate<'a> { // Write number of entries. VarInt(self.entries.len() as i32).encode(&mut w)?; - for entry in &self.entries { + for entry in self.entries.as_ref() { entry.player_uuid.encode(&mut w)?; if self.actions.add_player() { @@ -126,6 +127,9 @@ impl<'a> Decode<'a> for PlayerInfoUpdate<'a> { entries.push(entry); } - Ok(Self { actions, entries }) + Ok(Self { + actions, + entries: entries.into(), + }) } } diff --git a/crates/valence_protocol/src/packets/s2c/update_advancements.rs b/crates/valence_protocol/src/packets/s2c/update_advancements.rs index 06a580a..bb9c0a0 100644 --- a/crates/valence_protocol/src/packets/s2c/update_advancements.rs +++ b/crates/valence_protocol/src/packets/s2c/update_advancements.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::io::Write; use crate::{Decode, DecodePacket, Encode, EncodePacket, Ident, ItemStack, Text, VarInt}; @@ -26,8 +27,8 @@ pub struct AdvancementRequirements<'a> { #[derive(Clone, PartialEq, Debug)] pub struct AdvancementDisplay<'a> { - pub title: Text, - pub description: Text, + pub title: Cow<'a, Text>, + pub description: Cow<'a, Text>, pub icon: Option, pub frame_type: VarInt, pub flags: i32, @@ -66,8 +67,8 @@ impl Encode for AdvancementDisplay<'_> { impl<'a> Decode<'a> for AdvancementDisplay<'a> { fn decode(r: &mut &'a [u8]) -> anyhow::Result { - let title = Text::decode(r)?; - let description = Text::decode(r)?; + let title = >::decode(r)?; + let description = >::decode(r)?; let icon = Option::::decode(r)?; let frame_type = VarInt::decode(r)?; let flags = i32::decode(r)?; diff --git a/crates/valence_protocol/src/packets/s2c/update_teams.rs b/crates/valence_protocol/src/packets/s2c/update_teams.rs index 446c536..46848e6 100644 --- a/crates/valence_protocol/src/packets/s2c/update_teams.rs +++ b/crates/valence_protocol/src/packets/s2c/update_teams.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::io::Write; use anyhow::bail; @@ -8,24 +9,24 @@ use crate::{Decode, Encode, Text}; #[derive(Clone, PartialEq, Debug)] pub enum UpdateTeamsMode<'a> { CreateTeam { - team_display_name: Text, + team_display_name: Cow<'a, Text>, friendly_flags: TeamFlags, name_tag_visibility: NameTagVisibility, collision_rule: CollisionRule, team_color: TeamColor, - team_prefix: Text, - team_suffix: Text, + team_prefix: Cow<'a, Text>, + team_suffix: Cow<'a, Text>, entities: Vec<&'a str>, }, RemoveTeam, UpdateTeamInfo { - team_display_name: Text, + team_display_name: Cow<'a, Text>, friendly_flags: TeamFlags, name_tag_visibility: NameTagVisibility, collision_rule: CollisionRule, team_color: TeamColor, - team_prefix: Text, - team_suffix: Text, + team_prefix: Cow<'a, Text>, + team_suffix: Cow<'a, Text>, }, AddEntities { entities: Vec<&'a str>, diff --git a/crates/valence_protocol/src/text.rs b/crates/valence_protocol/src/text.rs index ce614a6..824002c 100644 --- a/crates/valence_protocol/src/text.rs +++ b/crates/valence_protocol/src/text.rs @@ -817,6 +817,18 @@ impl From for Text { } } +impl<'a> From for Cow<'a, Text> { + fn from(value: Text) -> Self { + Cow::Owned(value) + } +} + +impl<'a> From<&'a Text> for Cow<'a, Text> { + fn from(value: &'a Text) -> Self { + Cow::Borrowed(value) + } +} + impl fmt::Debug for Text { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.write_string(f) diff --git a/crates/valence_protocol/src/types.rs b/crates/valence_protocol/src/types.rs index fe98cd4..f334d06 100644 --- a/crates/valence_protocol/src/types.rs +++ b/crates/valence_protocol/src/types.rs @@ -137,17 +137,10 @@ pub enum StructureBlockRotation { } #[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode, Serialize, Deserialize)] -pub struct SignedProperty<'a> { - pub name: &'a str, - pub value: &'a str, - pub signature: Option<&'a str>, -} - -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, Serialize, Deserialize)] -pub struct SignedPropertyOwned { - pub name: String, - pub value: String, - pub signature: Option, +pub struct Property { + pub name: S, + pub value: S, + pub signature: Option, } #[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] @@ -366,6 +359,34 @@ pub struct Statistic { pub value: VarInt, } +#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] +pub enum WindowType { + Generic9x1, + Generic9x2, + Generic9x3, + Generic9x4, + Generic9x5, + Generic9x6, + Generic3x3, + Anvil, + Beacon, + BlastFurnace, + BrewingStand, + Crafting, + Enchantment, + Furnace, + Grindstone, + Hopper, + Lectern, + Loom, + Merchant, + ShulkerBox, + Smithing, + Smoker, + Cartography, + Stonecutter, +} + #[bitfield(u8)] #[derive(PartialEq, Eq, Encode, Decode)] pub struct EntityEffectFlags { diff --git a/crates/valence_protocol/src/username.rs b/crates/valence_protocol/src/username.rs index d6887e4..4e1f4f9 100644 --- a/crates/valence_protocol/src/username.rs +++ b/crates/valence_protocol/src/username.rs @@ -9,7 +9,7 @@ use anyhow::anyhow; use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize}; -use crate::{Decode, Encode, Result}; +use crate::{Decode, Encode, Result, Text}; /// A newtype wrapper around a string type `S` which guarantees the wrapped /// string meets the criteria for a valid Minecraft username. @@ -117,6 +117,15 @@ where } } +impl From> for Text +where + S: AsRef, +{ + fn from(value: Username) -> Self { + Text::text(value.as_str().to_owned()) + } +} + impl fmt::Display for Username where S: AsRef, diff --git a/crates/valence_derive/Cargo.toml b/crates/valence_protocol_macros/Cargo.toml similarity index 84% rename from crates/valence_derive/Cargo.toml rename to crates/valence_protocol_macros/Cargo.toml index 968208c..bc3ff98 100644 --- a/crates/valence_derive/Cargo.toml +++ b/crates/valence_protocol_macros/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "valence_derive" +name = "valence_protocol_macros" version = "0.1.0" edition = "2021" diff --git a/crates/valence_derive/src/decode.rs b/crates/valence_protocol_macros/src/decode.rs similarity index 100% rename from crates/valence_derive/src/decode.rs rename to crates/valence_protocol_macros/src/decode.rs diff --git a/crates/valence_derive/src/encode.rs b/crates/valence_protocol_macros/src/encode.rs similarity index 100% rename from crates/valence_derive/src/encode.rs rename to crates/valence_protocol_macros/src/encode.rs diff --git a/crates/valence_derive/src/lib.rs b/crates/valence_protocol_macros/src/lib.rs similarity index 100% rename from crates/valence_derive/src/lib.rs rename to crates/valence_protocol_macros/src/lib.rs diff --git a/extractor/build.gradle b/extractor/build.gradle index 645789a..83f8bb9 100644 --- a/extractor/build.gradle +++ b/extractor/build.gradle @@ -10,7 +10,7 @@ version = project.mod_version group = project.maven_group dependencies { - implementation 'com.google.code.gson:gson:2.9.0' + implementation 'com.google.code.gson:gson:2.10.1' // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" diff --git a/extractor/gradle.properties b/extractor/gradle.properties index 129744b..93d333b 100644 --- a/extractor/gradle.properties +++ b/extractor/gradle.properties @@ -1,16 +1,13 @@ # Done to increase the memory available to gradle. org.gradle.jvmargs=-Xmx1G - # Fabric Properties - # check these on https://fabricmc.net/develop - minecraft_version=1.19.3 - yarn_mappings=1.19.3+build.3 - loader_version=0.14.11 - +# check these on https://fabricmc.net/develop +minecraft_version=1.19.3 +yarn_mappings=1.19.3+build.3 +loader_version=0.14.11 # Mod Properties - mod_version = 1.0.0 - maven_group = dev.00a - archives_base_name = valence-extractor - +mod_version=1.0.0 +maven_group=dev.00a +archives_base_name=valence-extractor # Dependencies - fabric_version=0.69.1+1.19.3 +fabric_version=0.73.2+1.19.3