mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 14:31:30 +11:00
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 <dyc3@users.noreply.github.com> Co-authored-by: AviiNL <me@avii.nl> Co-authored-by: Danik Vitek <x3665107@gmail.com> Co-authored-by: Snowiiii <71594357+Snowiiii@users.noreply.github.com>
This commit is contained in:
parent
44ea6915db
commit
cb9230ec34
55
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
55
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
|
@ -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
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -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.
|
24
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
|
@ -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
|
29
.github/pull_request_template.md
vendored
Normal file
29
.github/pull_request_template.md
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!-- Please make sure that your PR is aligned with the guidelines in CONTRIBUTING.md to the best of your ability. -->
|
||||
<!-- Good PRs have tests! Make sure you have sufficient test coverage. -->
|
||||
|
||||
## Description
|
||||
|
||||
<!-- Describe the changes you've made. You may include any justification you want here. -->
|
||||
|
||||
## Test Plan
|
||||
|
||||
<!-- Explain how you tested your changes, and include any code that you used to test this. -->
|
||||
<!-- If there is an example that is sufficient to use in place of a playground, replace the playground section with a note that indicates this. -->
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Playground</summary>
|
||||
|
||||
```rust
|
||||
PASTE YOUR PLAYGROUND CODE HERE
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<!-- You need to include steps regardless of whether or not you are using a playground. -->
|
||||
Steps:
|
||||
1.
|
||||
|
||||
#### Related
|
||||
|
||||
<!-- Link to any issues that have context for this or that this PR fixes. -->
|
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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 <example_name>`.
|
||||
|
||||
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.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[workspace]
|
||||
members = ["crates/*"]
|
||||
exclude = ["crates/*/rust-mc-bot"]
|
||||
exclude = ["rust-mc-bot"]
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
|
101
README.md
101
README.md
|
@ -1,70 +1,88 @@
|
|||
<img src="assets/logo-full.svg" width="650">
|
||||
<p align="center">
|
||||
<img src="assets/logo-full.svg" width="650" align="center">
|
||||
</p>
|
||||
|
||||
_**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._
|
||||
|
||||
---
|
||||
<p align="center">
|
||||
<a href="https://github.com/valence-rs/valence/blob/main/LICENSE.txt">
|
||||
<img src="https://img.shields.io/github/license/valence-rs/valence"
|
||||
alt="license"></a>
|
||||
<a href="https://crates.io/crates/valence">
|
||||
<img src="https://img.shields.io/crates/d/valence?label=crates.io"></a>
|
||||
<a href="https://discord.gg/8Fqqy9XrYb">
|
||||
<img src="https://img.shields.io/discord/998132822239870997?logo=discord"
|
||||
alt="chat on Discord"></a>
|
||||
<a href="https://github.com/sponsors/rj00a">
|
||||
<img src="https://img.shields.io/github/sponsors/rj00a"
|
||||
alt="GitHub sponsors"></a>
|
||||
</p>
|
||||
|
||||
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!
|
||||
|
|
|
@ -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.
|
|
@ -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<PlayerListId>,
|
||||
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<Dimension> {
|
||||
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<Self>,
|
||||
_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<Self>) {
|
||||
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<Self>) {
|
||||
{
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<P>
|
||||
where
|
||||
P: DecodePacket<'a> + EncodePacket + fmt::Debug,
|
||||
P: DecodePacket<'a> + EncodePacket,
|
||||
{
|
||||
while !self.dec.has_next_packet()? {
|
||||
self.dec.reserve(4096);
|
||||
|
|
|
@ -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" }
|
21
crates/playground/build.rs
Normal file
21
crates/playground/build.rs
Normal file
|
@ -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();
|
||||
}
|
1
crates/playground/src/.gitignore
vendored
Normal file
1
crates/playground/src/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
playground.rs
|
2
crates/playground/src/extras.rs
Normal file
2
crates/playground/src/extras.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
//! Put stuff in here if you find that you have to write the same code for
|
||||
//! multiple playgrounds.
|
12
crates/playground/src/main.rs
Normal file
12
crates/playground/src/main.rs
Normal file
|
@ -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();
|
||||
}
|
51
crates/playground/src/playground.template.rs
Normal file
51
crates/playground/src/playground.template.rs
Normal file
|
@ -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::<Server>()
|
||||
.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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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!
|
|
@ -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 <ryanj00a@gmail.com>"]
|
||||
|
@ -14,18 +14,20 @@ authors = ["Ryan Johnson <ryanj00a@gmail.com>"]
|
|||
[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"
|
||||
|
|
|
@ -128,7 +128,7 @@ impl Value {
|
|||
Value::Facing(_) => quote!(Facing),
|
||||
Value::OptionalUuid(_) => quote!(Option<Uuid>),
|
||||
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)
|
||||
|
|
|
@ -8,13 +8,13 @@ use serde::Deserialize;
|
|||
use crate::ident;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
struct EntityData {
|
||||
struct EntityEvents {
|
||||
statuses: BTreeMap<String, u8>,
|
||||
animations: BTreeMap<String, u8>,
|
||||
}
|
||||
|
||||
pub fn build() -> anyhow::Result<TokenStream> {
|
||||
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<TokenStream> {
|
|||
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)*
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
86
crates/valence/examples/bench_players.rs
Normal file
86
crates/valence/examples/bench_players.rs
Normal file
|
@ -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<Server>, time: Res<TickStart>, clients: Query<(), With<Client>>) {
|
||||
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::<Server>()
|
||||
.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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<PlayerListId>,
|
||||
}
|
||||
|
||||
#[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<Dimension> {
|
||||
vec![Dimension {
|
||||
fixed_time: Some(6000),
|
||||
..Dimension::default()
|
||||
}]
|
||||
}
|
||||
|
||||
fn biomes(&self) -> Vec<Biome> {
|
||||
(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<Self>,
|
||||
_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<Self>) {
|
||||
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<Self>) {
|
||||
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::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
.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::<Server>();
|
||||
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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<Server>()
|
||||
.new_instance(DimensionId::default());
|
||||
|
||||
struct ServerState {
|
||||
player_list: Option<PlayerListId>,
|
||||
}
|
||||
|
||||
#[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<Dimension> {
|
||||
vec![Dimension {
|
||||
fixed_time: Some(6000),
|
||||
..Dimension::default()
|
||||
}]
|
||||
}
|
||||
|
||||
async fn server_list_ping(
|
||||
&self,
|
||||
_server: &SharedServer<Self>,
|
||||
_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<Self>) {
|
||||
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<Self>) {
|
||||
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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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<StartSneaking>,
|
||||
) {
|
||||
for event in events.iter() {
|
||||
let Ok(mut client) = clients.get_component_mut::<Client>(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<StartDigging>,
|
||||
) {
|
||||
let mut instance = instances.single_mut();
|
||||
|
||||
for event in events.iter() {
|
||||
let Ok(client) = clients.get_component::<Client>(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<FinishDigging>,
|
||||
) {
|
||||
let mut instance = instances.single_mut();
|
||||
|
||||
for event in events.iter() {
|
||||
let Ok(client) = clients.get_component::<Client>(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<UseItemOnBlock>,
|
||||
) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<Server>()
|
||||
.new_instance(DimensionId::default());
|
||||
|
||||
struct ServerState {
|
||||
player_list: Option<PlayerListId>,
|
||||
chest: InventoryId,
|
||||
tick: u32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientState {
|
||||
entity_id: EntityId,
|
||||
// open_inventory: Option<WindowInventory>,
|
||||
}
|
||||
|
||||
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<Dimension> {
|
||||
vec![Dimension {
|
||||
fixed_time: Some(6000),
|
||||
..Dimension::default()
|
||||
}]
|
||||
}
|
||||
|
||||
async fn server_list_ping(
|
||||
&self,
|
||||
_server: &SharedServer<Self>,
|
||||
_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<Self>) {
|
||||
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<Self>) {
|
||||
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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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<StartSneaking>,
|
||||
) {
|
||||
for event in events.iter() {
|
||||
let Ok(mut client) = clients.get_component_mut::<Client>(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<Entity, (With<Inventory>, Without<Client>)>,
|
||||
mut events: EventReader<UseItemOnBlock>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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::<Server>()
|
||||
.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<f64>,
|
||||
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<PlayerListId>;
|
||||
type ClientState = ClientState;
|
||||
type EntityState = EntityState;
|
||||
type WorldState = ();
|
||||
type ChunkState = ();
|
||||
type PlayerListState = ();
|
||||
type InventoryState = ();
|
||||
|
||||
async fn server_list_ping(
|
||||
&self,
|
||||
_server: &SharedServer<Self>,
|
||||
_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<Self>) {
|
||||
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::<f64>() < 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<Self>) {
|
||||
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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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<McEntityManager>,
|
||||
server: Res<Server>,
|
||||
mut start_sprinting: EventReader<StartSprinting>,
|
||||
mut stop_sprinting: EventReader<StopSprinting>,
|
||||
mut interact_with_entity: EventReader<InteractWithEntity>,
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<Server>()
|
||||
.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<PlayerListId>,
|
||||
paused: bool,
|
||||
fn init_clients(
|
||||
mut clients: Query<&mut Client, Added<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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<Dimension> {
|
||||
vec![Dimension {
|
||||
fixed_time: Some(6000),
|
||||
..Dimension::default()
|
||||
}]
|
||||
}
|
||||
|
||||
fn biomes(&self) -> Vec<Biome> {
|
||||
vec![Biome {
|
||||
name: ident!("plains"),
|
||||
grass_color: Some(0x00ff00),
|
||||
..Biome::default()
|
||||
}]
|
||||
}
|
||||
|
||||
async fn server_list_ping(
|
||||
&self,
|
||||
_server: &SharedServer<Self>,
|
||||
_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<Self>) {
|
||||
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<Self>) {
|
||||
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<StartDigging>, mut board: ResMut<LifeBoard>) {
|
||||
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<LifeBoard>,
|
||||
mut instances: Query<&mut Instance>,
|
||||
server: Res<Server>,
|
||||
) {
|
||||
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<StartSneaking>,
|
||||
mut board: ResMut<LifeBoard>,
|
||||
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<LifeBoard>) {
|
||||
for mut client in &mut clients {
|
||||
if client.position().y < 0.0 {
|
||||
client.set_position(SPAWN_POS);
|
||||
board.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<Server>()
|
||||
.new_instance(DimensionId::default());
|
||||
|
||||
struct ServerState {
|
||||
player_list: Option<PlayerListId>,
|
||||
cows: Vec<EntityId>,
|
||||
}
|
||||
|
||||
#[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<Self>,
|
||||
_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<Self>) {
|
||||
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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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<Self>) {
|
||||
let current_tick = server.current_tick();
|
||||
let (world_id, _) = server.worlds.iter_mut().next().expect("missing world");
|
||||
fn update_sphere(server: Res<Server>, mut parts: Query<&mut McEntity, With<SpherePart>>) {
|
||||
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<Item = Vec3<f64>> {
|
||||
fn fibonacci_spiral(n: usize) -> impl Iterator<Item = DVec3> {
|
||||
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<Item = Vec3<f64>> {
|
|||
// 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
|
||||
}
|
||||
|
|
|
@ -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::<Server>()
|
||||
.new_instance(DimensionId::default());
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientState {
|
||||
entity_id: EntityId,
|
||||
// World and position to respawn at
|
||||
respawn_location: (WorldId, Vec3<f64>),
|
||||
// Anticheat measure
|
||||
can_respawn: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ServerState {
|
||||
player_list: Option<PlayerListId>,
|
||||
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<f64> {
|
||||
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<Dimension> {
|
||||
vec![
|
||||
Dimension {
|
||||
fixed_time: Some(6000),
|
||||
..Dimension::default()
|
||||
},
|
||||
Dimension {
|
||||
fixed_time: Some(19000),
|
||||
..Dimension::default()
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async fn server_list_ping(
|
||||
&self,
|
||||
_server: &SharedServer<Self>,
|
||||
_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<Self>) {
|
||||
// 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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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<StartSneaking>) {
|
||||
for event in events.iter() {
|
||||
let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else {
|
||||
warn!("Client {:?} not found", event.client);
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
fn update(&self, server: &mut Server<Self>) {
|
||||
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<Game>, 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<PerformRespawn>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
for event in events.iter() {
|
||||
let Ok(mut client) = clients.get_component_mut::<Client>(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
|
||||
}
|
||||
|
|
|
@ -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<PlayerListId>,
|
||||
bvh: Bvh<WithAabb<EntityId>>,
|
||||
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<Self>,
|
||||
_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<Self>) {
|
||||
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<Self>) {
|
||||
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<WithAabb<EntityId>>| {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
82
crates/valence/examples/gamemode_switcher.rs
Normal file
82
crates/valence/examples/gamemode_switcher.rs
Normal file
|
@ -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::<Server>()
|
||||
.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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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<ChatCommand>) {
|
||||
for event in events.iter() {
|
||||
let Ok(mut client) = clients.get_component_mut::<Client>(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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<PlayerListId>,
|
||||
}
|
||||
|
||||
#[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<Dimension> {
|
||||
vec![Dimension {
|
||||
fixed_time: Some(6000),
|
||||
..Dimension::default()
|
||||
}]
|
||||
}
|
||||
|
||||
async fn server_list_ping(
|
||||
&self,
|
||||
_server: &SharedServer<Self>,
|
||||
_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<Self>) {
|
||||
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<Self>) {
|
||||
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<Game>, player: &mut Entity<Game>, 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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<BlockPos>,
|
||||
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<PlayerListId>;
|
||||
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<Self>,
|
||||
_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<Self>) {
|
||||
server.state = Some(server.player_lists.insert(()).0);
|
||||
}
|
||||
|
||||
fn update(&self, server: &mut Server<Self>) {
|
||||
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<Game>, world: &mut World<Game>) {
|
||||
// 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<BlockPos>,
|
||||
score: u32,
|
||||
combo: u32,
|
||||
target_y: i32,
|
||||
last_block_timestamp: u128,
|
||||
}
|
||||
|
||||
fn init_clients(
|
||||
mut commands: Commands,
|
||||
server: Res<Server>,
|
||||
mut clients: Query<(Entity, &mut Client), Added<Client>>,
|
||||
) {
|
||||
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<Game>, world: &mut World<Game>, 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<GameState>>,
|
||||
) {
|
||||
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);
|
||||
|
||||
|
|
|
@ -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<Particle>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
struct ServerState {
|
||||
player_list: Option<PlayerListId>,
|
||||
particle_list: Vec<Particle>,
|
||||
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<Self>,
|
||||
_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<Self>) {
|
||||
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<Self>) {
|
||||
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::<Server>()
|
||||
.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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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<ParticleSpawner>,
|
||||
server: Res<Server>,
|
||||
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> {
|
|||
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 {
|
||||
|
|
120
crates/valence/examples/player_list.rs
Normal file
120
crates/valence/examples/player_list.rs
Normal file
|
@ -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::<Server>()
|
||||
.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::<PlayerList>();
|
||||
|
||||
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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
mut player_list: ResMut<PlayerList>,
|
||||
) {
|
||||
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<PlayerList>, server: Res<Server>) {
|
||||
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<PlayerList>,
|
||||
) {
|
||||
for client in &clients {
|
||||
if client.is_disconnected() {
|
||||
player_list.remove(client.uuid());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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::<Server>()
|
||||
.new_instance(DimensionId::default());
|
||||
|
||||
struct ServerState {
|
||||
player_list: Option<PlayerListId>,
|
||||
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<Self>,
|
||||
_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<Self>) {
|
||||
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<Self>) {
|
||||
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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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<Game>) {
|
||||
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<InteractWithEntity>) {
|
||||
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<ResourcePackStatusChange>,
|
||||
) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
37
crates/valence/examples/server_list_ping.rs
Normal file
37
crates/valence/examples/server_list_ping.rs
Normal file
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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<ChunkPos>,
|
||||
// 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<ChunkPos, Option<Priority>>,
|
||||
sender: Sender<ChunkPos>,
|
||||
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::<Server>()
|
||||
.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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
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<PlayerListId>;
|
||||
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<GameState>,
|
||||
) {
|
||||
let instance = instances.single_mut();
|
||||
|
||||
async fn server_list_ping(
|
||||
&self,
|
||||
_server: &SharedServer<Self>,
|
||||
_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<GameState>) {
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
let (world_id, world) = server.worlds.iter_mut().next().unwrap();
|
||||
fn chunk_worker(state: Arc<ChunkWorkerState>) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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::<Server>()
|
||||
.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<f64> = 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<Self>,
|
||||
_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<Self>) {
|
||||
server.state = ServerState {
|
||||
world: create_world(server),
|
||||
};
|
||||
}
|
||||
|
||||
fn update(&self, server: &mut Server<Self>) {
|
||||
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<Game>) -> 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<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<BiomeAdditionsSound>,
|
||||
pub mood_sound: Option<BiomeMoodSound>,
|
||||
pub particle: Option<BiomeParticle>,
|
||||
// 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
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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<EntityId>,
|
||||
/// Entities that have entered the chunk this tick, paired with the cell
|
||||
/// position in this world they came from.
|
||||
incoming: Vec<(EntityId, Option<ChunkPos>)>,
|
||||
/// Entities that have left the chunk this tick, paired with the cell
|
||||
/// position in this world they arrived at.
|
||||
outgoing: Vec<(EntityId, Option<ChunkPos>)>,
|
||||
/// A cache of packets needed to update all the `entities` in this chunk.
|
||||
cached_update_packets: Vec<u8>,
|
||||
}
|
||||
|
||||
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<Item = EntityId> + '_ {
|
||||
self.entities.iter().cloned()
|
||||
}
|
||||
|
||||
pub fn incoming(&self) -> &[(EntityId, Option<ChunkPos>)] {
|
||||
&self.incoming
|
||||
}
|
||||
|
||||
pub fn outgoing(&self) -> &[(EntityId, Option<ChunkPos>)] {
|
||||
&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<C: Config>(
|
||||
entities: &mut Entities<C>,
|
||||
worlds: &mut Worlds<C>,
|
||||
compression_threshold: Option<u32>,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Item = Self> {
|
||||
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<F>(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<ChunkPos> 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<ChunkPos> for [i32; 2] {
|
||||
fn from(pos: ChunkPos) -> Self {
|
||||
[pos.x, pos.z]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockPos> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -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<A> {
|
||||
pub callbacks: Arc<A>,
|
||||
/// 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<Handle>,
|
||||
/// 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<u32>,
|
||||
/// 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<A: AsyncCallbacks> ServerPlugin<A> {
|
||||
pub fn new(callbacks: impl Into<Arc<A>>) -> 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<Handle>) -> 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<u32>) -> 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<Arc<[Dimension]>>) -> Self {
|
||||
self.dimensions = dimensions.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// See [`Self::biomes`].
|
||||
#[must_use]
|
||||
pub fn with_biomes(mut self, biomes: impl Into<Arc<[Biome]>>) -> Self {
|
||||
self.biomes = biomes.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: AsyncCallbacks + Default> Default for ServerPlugin<A> {
|
||||
fn default() -> Self {
|
||||
Self::new(A::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: AsyncCallbacks> Plugin for ServerPlugin<A> {
|
||||
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<u32> {
|
||||
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=<username>&serverId=<auth-digest>&ip=<player-ip>`.
|
||||
///
|
||||
/// [online mode]: crate::config::ConnectionMode::Online
|
||||
fn session_server(
|
||||
async fn session_server(
|
||||
&self,
|
||||
server: &SharedServer<Self>,
|
||||
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<TokioHandle> {
|
||||
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<Dimension> {
|
||||
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<Biome> {
|
||||
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<Self>,
|
||||
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<Self>, 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<Self>) {}
|
||||
|
||||
/// 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<Self>) {}
|
||||
}
|
||||
|
||||
/// 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<Cow<'a, [u8]>>,
|
||||
},
|
||||
/// 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<str>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A minimal `Config` implementation for testing purposes.
|
||||
#[cfg(test)]
|
||||
pub(crate) struct MockConfig<S = (), Cl = (), E = (), W = (), Ch = (), P = (), I = ()> {
|
||||
_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<PlayerSampleEntry>,
|
||||
/// 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<S, Cl, E, W, Ch, P, I> Config for MockConfig<S, Cl, E, W, Ch, P, I>
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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::*;
|
||||
|
|
550
crates/valence/src/instance.rs
Normal file
550
crates/valence/src/instance.rs
Normal file
|
@ -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::<Server>().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::<Server>().unwrap();
|
||||
/// # let instance = server.new_instance(DimensionId::default());
|
||||
/// let instance_entity = app.world.spawn(instance);
|
||||
/// ```
|
||||
#[derive(Component)]
|
||||
pub struct Instance {
|
||||
pub(crate) partition: FxHashMap<ChunkPos, PartitionCell>,
|
||||
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<u8>,
|
||||
/// Scratch space for writing packets.
|
||||
scratch: Vec<u8>,
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceInfo {
|
||||
dimension: DimensionId,
|
||||
section_count: usize,
|
||||
min_y: i32,
|
||||
biome_registry_len: usize,
|
||||
compression_threshold: Option<u32>,
|
||||
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<u8, 2048>]>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PartitionCell {
|
||||
/// The chunk in this cell.
|
||||
pub(crate) chunk: Option<Chunk<true>>,
|
||||
/// If `chunk` went from `Some` to `None` this tick.
|
||||
pub(crate) chunk_removed: bool,
|
||||
/// Minecraft entities in this cell.
|
||||
pub(crate) entities: BTreeSet<Entity>,
|
||||
/// 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<ChunkPos>)>,
|
||||
/// 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<ChunkPos>)>,
|
||||
/// 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<u8>,
|
||||
}
|
||||
|
||||
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<ChunkPos>) -> Option<&Chunk<true>> {
|
||||
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<ChunkPos>) -> Option<&mut Chunk<true>> {
|
||||
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<ChunkPos>, chunk: Chunk) -> Option<Chunk> {
|
||||
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<ChunkPos>) -> Option<Chunk> {
|
||||
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<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(ChunkPos, &mut Chunk<true>) -> 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<ChunkPos>) -> 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<Item = (ChunkPos, &Chunk<true>)> + 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<Item = (ChunkPos, &mut Chunk<true>)> + '_ {
|
||||
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<BlockPos>) -> 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<BlockPos>, 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<P>(&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<P>(&mut self, pkt: &P, pos: impl Into<ChunkPos>)
|
||||
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<ChunkPos>) {
|
||||
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<DVec3>,
|
||||
offset: impl Into<Vec3>,
|
||||
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<Text>) {
|
||||
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<Server>,
|
||||
) {
|
||||
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;
|
||||
}
|
568
crates/valence/src/instance/chunk.rs
Normal file
568
crates/valence/src/instance/chunk.rs
Normal file
|
@ -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<const LOADED: bool = false> {
|
||||
sections: Vec<Section>,
|
||||
/// Cached bytes of the chunk data packet. The cache is considered
|
||||
/// invalidated if empty.
|
||||
cached_init_packets: Mutex<Vec<u8>>,
|
||||
/// 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<BlockState, SECTION_BLOCK_COUNT, { SECTION_BLOCK_COUNT / 2 }>,
|
||||
biomes: PalettedContainer<BiomeId, SECTION_BIOME_COUNT, { SECTION_BIOME_COUNT / 2 }>,
|
||||
/// 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<VarLong>,
|
||||
}
|
||||
|
||||
const SECTION_BLOCK_COUNT: usize = 16 * 16 * 16;
|
||||
const SECTION_BIOME_COUNT: usize = 4 * 4 * 4;
|
||||
|
||||
impl Chunk<false> {
|
||||
/// 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<true> {
|
||||
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<true> {
|
||||
/// 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<false> {
|
||||
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<u8>,
|
||||
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<u8>,
|
||||
) {
|
||||
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<const LOADED: bool> Chunk<LOADED> {
|
||||
/// 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<const LOADED: bool>(chunk: &Chunk<LOADED>, 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);
|
||||
}
|
||||
}
|
120
crates/valence/src/instance/chunk_entry.rs
Normal file
120
crates/valence/src/instance/chunk_entry.rs
Normal file
|
@ -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<true> {
|
||||
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<true> {
|
||||
self.entry.get().chunk.as_ref().unwrap()
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self) -> &mut Chunk<true> {
|
||||
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<true> {
|
||||
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<true> {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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<T: Copy + Eq + Default, const LEN: usize, const HALF_LEN: usize>
|
|||
}
|
||||
|
||||
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<T: Copy + Eq + Default, const LEN: usize, const HALF_LEN: usize>
|
|||
}
|
||||
|
||||
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<T: Copy + Eq + Default, const LEN: usize, const HALF_LEN: usize>
|
|||
}
|
||||
}
|
||||
|
||||
#[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<T: Copy + Eq + Default, const LEN: usize, const HALF_LEN: usize> Indirect<T
|
|||
}
|
||||
}
|
||||
|
||||
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<Item = u64>,
|
||||
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;
|
File diff suppressed because it is too large
Load diff
|
@ -1,79 +1,17 @@
|
|||
//! <img src="https://raw.githubusercontent.com/rj00a/valence/main/assets/logo-full.svg" width="400">
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! 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);
|
||||
|
|
88
crates/valence/src/math.rs
Normal file
88
crates/valence/src/math.rs
Normal file
|
@ -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<DVec3>, p1: impl Into<DVec3>) -> 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<DVec3>, size: impl Into<DVec3>) -> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<P>(&mut self, packet: &P) -> anyhow::Result<()>
|
||||
pub(crate) trait WritePacket {
|
||||
fn write_packet<P>(&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<W: WritePacket> WritePacket for &mut W {
|
||||
fn write_packet<P>(&mut self, packet: &P) -> anyhow::Result<()>
|
||||
fn write_packet<P>(&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<u8>,
|
||||
threshold: Option<u32>,
|
||||
scratch: &'a mut Vec<u8>,
|
||||
|
@ -40,18 +41,39 @@ impl<'a> PacketWriter<'a> {
|
|||
}
|
||||
|
||||
impl WritePacket for PacketWriter<'_> {
|
||||
fn write_packet<P>(&mut self, pkt: &P) -> anyhow::Result<()>
|
||||
fn write_packet<P>(&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<P>(&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)
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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<str>,
|
||||
signature: Box<str>,
|
||||
skin_url: Box<str>,
|
||||
cape_url: Option<Box<str>>,
|
||||
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<Url>,
|
||||
}
|
||||
|
||||
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<Box<str>>,
|
||||
signature: impl Into<Box<str>>,
|
||||
) -> anyhow::Result<Self> {
|
||||
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<Self> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<C: Config> {
|
||||
/// Custom state.
|
||||
pub state: C::ServerState,
|
||||
/// A handle to this server's [`SharedServer`].
|
||||
pub shared: SharedServer<C>,
|
||||
/// All of the clients on the server.
|
||||
pub clients: Clients<C>,
|
||||
/// All of entities on the server.
|
||||
pub entities: Entities<C>,
|
||||
/// All of the worlds on the server.
|
||||
pub worlds: Worlds<C>,
|
||||
/// All of the player lists on the server.
|
||||
pub player_lists: PlayerLists<C>,
|
||||
/// All of the inventories on the server.
|
||||
pub inventories: Inventories<C>,
|
||||
/// 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<C: Config> Server<C> {
|
||||
/// 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<C: Config> Deref for Server<C> {
|
||||
type Target = C::ServerState;
|
||||
impl Deref for Server {
|
||||
type Target = SharedServer;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.state
|
||||
&self.shared
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Config> DerefMut for Server<C> {
|
||||
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<C: Config>(Arc<SharedServerInner<C>>);
|
||||
/// `SharedServer`s are internally refcounted and are inexpensive to clone.
|
||||
#[derive(Clone)]
|
||||
pub struct SharedServer(Arc<SharedServerInner>);
|
||||
|
||||
impl<C: Config> Clone for SharedServer<C> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct SharedServerInner<C: Config> {
|
||||
cfg: C,
|
||||
struct SharedServerInner {
|
||||
address: SocketAddr,
|
||||
tick_rate: Ticks,
|
||||
tps: i64,
|
||||
connection_mode: ConnectionMode,
|
||||
compression_threshold: Option<u32>,
|
||||
max_connections: usize,
|
||||
|
@ -131,58 +92,36 @@ struct SharedServerInner<C: Config> {
|
|||
/// 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<Runtime>,
|
||||
dimensions: Vec<Dimension>,
|
||||
biomes: Vec<Biome>,
|
||||
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<Client>,
|
||||
/// Receiver for new clients past the login stage.
|
||||
new_clients_send: Sender<NewClientMessage>,
|
||||
new_clients_recv: Receiver<NewClientMessage>,
|
||||
new_clients_recv: Receiver<Client>,
|
||||
/// A semaphore used to limit the number of simultaneous connections to the
|
||||
/// server. Closing this semaphore stops new connections.
|
||||
connection_sema: Arc<Semaphore>,
|
||||
/// The result that will be returned when the server is shut down.
|
||||
shutdown_result: Mutex<Option<ShutdownResult>>,
|
||||
shutdown_result: Mutex<Option<anyhow::Result<()>>>,
|
||||
/// 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<String>,
|
||||
/// 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<SignedPlayerTextures>,
|
||||
}
|
||||
|
||||
struct NewClientMessage {
|
||||
ncd: NewClientData,
|
||||
send: PlayPacketSender,
|
||||
recv: PlayPacketReceiver,
|
||||
permit: OwnedSemaphorePermit,
|
||||
}
|
||||
|
||||
/// The result type returned from [`start_server`].
|
||||
pub type ShutdownResult = Result<(), Box<dyn Error + Send + Sync + 'static>>;
|
||||
|
||||
impl<C: Config> SharedServer<C> {
|
||||
/// 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<C: Config> SharedServer<C> {
|
|||
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<C: Config> SharedServer<C> {
|
|||
}
|
||||
|
||||
/// 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<C: Config> SharedServer<C> {
|
|||
}
|
||||
|
||||
/// 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<C: Config> SharedServer<C> {
|
|||
}
|
||||
|
||||
/// 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<E>(&self, res: Result<(), E>)
|
||||
where
|
||||
E: Into<Box<dyn Error + Send + Sync + 'static>>,
|
||||
E: Into<anyhow::Error>,
|
||||
{
|
||||
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<C: Config>(config: C, data: C::ServerState) -> ShutdownResult {
|
||||
let shared = setup_server(config)
|
||||
.context("failed to initialize server")
|
||||
.map_err(Box::<dyn Error + Send + Sync + 'static>::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<String>,
|
||||
/// 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<Property>,
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn setup_server<C: Config>(cfg: C) -> anyhow::Result<SharedServer<C>> {
|
||||
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<impl AsyncCallbacks>,
|
||||
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<C: Config>(cfg: C) -> anyhow::Result<SharedServer<C>> {
|
|||
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<C: Config>(cfg: C) -> anyhow::Result<SharedServer<C>> {
|
|||
|
||||
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::<AppExit>::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::<Events<AppExit>>() {
|
||||
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<Entity, With<Despawned>>) {
|
||||
for entity in &entities {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
fn inc_current_tick(mut server: ResMut<Server>) {
|
||||
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<impl Config>) -> 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<impl Config>) {
|
||||
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<impl Config>,
|
||||
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::<io::Error>() {
|
||||
if e.kind() == io::ErrorKind::UnexpectedEof {
|
||||
return;
|
||||
}
|
||||
}
|
||||
warn!("connection ended with error: {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_handshake(
|
||||
server: SharedServer<impl Config>,
|
||||
mut mngr: InitialPacketManager<OwnedReadHalf, OwnedWriteHalf>,
|
||||
remote_addr: SocketAddr,
|
||||
) -> anyhow::Result<()> {
|
||||
let handshake = mngr.recv_packet::<HandshakeOwned>().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<impl Config>,
|
||||
mut mngr: InitialPacketManager<OwnedReadHalf, OwnedWriteHalf>,
|
||||
remote_addr: SocketAddr,
|
||||
handshake: HandshakeOwned,
|
||||
) -> anyhow::Result<()> {
|
||||
mngr.recv_packet::<StatusRequest>().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<impl Config>,
|
||||
mngr: &mut InitialPacketManager<OwnedReadHalf, OwnedWriteHalf>,
|
||||
remote_addr: SocketAddr,
|
||||
handshake: HandshakeOwned,
|
||||
) -> anyhow::Result<Option<NewClientData>> {
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
503
crates/valence/src/server/connect.rs
Normal file
503
crates/valence/src/server/connect.rs
Normal file
|
@ -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<impl AsyncCallbacks>) {
|
||||
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<impl AsyncCallbacks>,
|
||||
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::<io::Error>() {
|
||||
if e.kind() == io::ErrorKind::UnexpectedEof {
|
||||
return;
|
||||
}
|
||||
}
|
||||
warn!("connection ended with error: {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_handshake(
|
||||
shared: SharedServer,
|
||||
callbacks: Arc<impl AsyncCallbacks>,
|
||||
mut conn: InitialConnection<OwnedReadHalf, OwnedWriteHalf>,
|
||||
remote_addr: SocketAddr,
|
||||
) -> anyhow::Result<()> {
|
||||
let handshake = conn.recv_packet::<HandshakeOwned>().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<impl AsyncCallbacks>,
|
||||
mut conn: InitialConnection<OwnedReadHalf, OwnedWriteHalf>,
|
||||
remote_addr: SocketAddr,
|
||||
handshake: HandshakeOwned,
|
||||
) -> anyhow::Result<()> {
|
||||
conn.recv_packet::<StatusRequest>().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<impl AsyncCallbacks>,
|
||||
conn: &mut InitialConnection<OwnedReadHalf, OwnedWriteHalf>,
|
||||
remote_addr: SocketAddr,
|
||||
handshake: HandshakeOwned,
|
||||
) -> anyhow::Result<Option<NewClientInfo>> {
|
||||
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<impl AsyncCallbacks>,
|
||||
conn: &mut InitialConnection<OwnedReadHalf, OwnedWriteHalf>,
|
||||
remote_addr: SocketAddr,
|
||||
username: Username<String>,
|
||||
) -> anyhow::Result<NewClientInfo> {
|
||||
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<String>,
|
||||
properties: Vec<Property>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
) -> anyhow::Result<NewClientInfo> {
|
||||
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<String>,
|
||||
) -> anyhow::Result<NewClientInfo> {
|
||||
// Get data from server_address field of the handshake
|
||||
let [_, client_ip, uuid, properties]: [&str; 4] = server_address
|
||||
.split('\0')
|
||||
.take(4)
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.map_err(|_| anyhow!("malformed BungeeCord server address data"))?;
|
||||
|
||||
// Read properties and get textures
|
||||
let properties: Vec<Property> =
|
||||
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<OwnedReadHalf, OwnedWriteHalf>,
|
||||
username: Username<String>,
|
||||
velocity_secret: &str,
|
||||
) -> anyhow::Result<NewClientInfo> {
|
||||
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::<Sha256>::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::<Property>::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"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<R, W> {
|
||||
pub(super) struct InitialConnection<R, W> {
|
||||
reader: R,
|
||||
writer: W,
|
||||
enc: PacketEncoder,
|
||||
|
@ -26,7 +28,7 @@ pub struct InitialPacketManager<R, W> {
|
|||
|
||||
const READ_BUF_SIZE: usize = 4096;
|
||||
|
||||
impl<R, W> InitialPacketManager<R, W>
|
||||
impl<R, W> InitialConnection<R, W>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
W: AsyncWrite + Unpin,
|
||||
|
@ -49,7 +51,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn send_packet<P>(&mut self, pkt: &P) -> Result<()>
|
||||
pub async fn send_packet<P>(&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<P>
|
||||
pub async fn recv_packet<'a, P>(&'a mut self) -> anyhow::Result<P>
|
||||
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<JoinHandle<()>>,
|
||||
handle: Handle,
|
||||
}
|
||||
|
||||
impl PlayPacketSender {
|
||||
pub fn append_packet<P>(&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<P>(&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<P>(&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<Option<P>>
|
||||
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<BytesMut> {
|
||||
match self.recv.try_recv() {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
Err(TryRecvError::Empty) => Ok(BytesMut::new()),
|
||||
Err(TryRecvError::Disconnected) => bail!("client disconnected"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<impl Config>,
|
||||
mngr: &mut InitialPacketManager<OwnedReadHalf, OwnedWriteHalf>,
|
||||
remote_addr: SocketAddr,
|
||||
username: Username<String>,
|
||||
) -> anyhow::Result<NewClientData> {
|
||||
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<String>,
|
||||
properties: Vec<SignedPropertyOwned>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
) -> anyhow::Result<NewClientData> {
|
||||
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<String>,
|
||||
) -> anyhow::Result<NewClientData> {
|
||||
// Get data from server_address field of the handshake
|
||||
let [_, client_ip, uuid, properties]: [&str; 4] = server_address
|
||||
.split('\0')
|
||||
.take(4)
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.map_err(|_| anyhow!("malformed BungeeCord server address data"))?;
|
||||
|
||||
// Read properties and get textures
|
||||
let properties: Vec<SignedProperty> =
|
||||
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<OwnedReadHalf, OwnedWriteHalf>,
|
||||
username: Username<String>,
|
||||
velocity_secret: &str,
|
||||
) -> anyhow::Result<NewClientData> {
|
||||
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::<Sha256>::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::<SignedProperty>::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"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
entries: Vec<Entry<T>>,
|
||||
next_free_head: usize,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl<T> Default for Slab<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Entry<T> {
|
||||
Occupied(T),
|
||||
Vacant { next_free: usize },
|
||||
}
|
||||
|
||||
impl<T> Slab<T> {
|
||||
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<T> {
|
||||
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<T> {
|
||||
Iter {
|
||||
entries: self.entries.iter().enumerate(),
|
||||
len: self.len,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> IterMut<T> {
|
||||
IterMut {
|
||||
entries: self.entries.iter_mut().enumerate(),
|
||||
len: self.len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoIterator for &'a Slab<T> {
|
||||
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<T> {
|
||||
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<T> {
|
||||
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<T> {
|
||||
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<slice::Iter<'a, Entry<T>>>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
pub struct IterMut<'a, T> {
|
||||
entries: iter::Enumerate<slice::IterMut<'a, Entry<T>>>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
pub struct ParIter<'a, T> {
|
||||
slab: &'a Slab<T>,
|
||||
}
|
||||
|
||||
pub struct ParIterMut<'a, T> {
|
||||
slab: &'a mut Slab<T>,
|
||||
}
|
||||
|
||||
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<Self::Item> {
|
||||
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<usize>) {
|
||||
(self.len, Some(self.len))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DoubleEndedIterator for Iter<'_, T> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
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<T> ExactSizeIterator for Iter<'_, T> {
|
||||
fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FusedIterator for Iter<'_, T> {}
|
||||
|
||||
impl<'a, T> Iterator for IterMut<'a, T> {
|
||||
type Item = (usize, &'a mut T);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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<usize>) {
|
||||
(self.len, Some(self.len))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DoubleEndedIterator for IterMut<'_, T> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
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<T> ExactSizeIterator for IterMut<'_, T> {
|
||||
fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FusedIterator for IterMut<'_, T> {}
|
||||
|
||||
impl<T> 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<C>(self, consumer: C) -> C::Result
|
||||
where
|
||||
C: UnindexedConsumer<Self::Item>,
|
||||
{
|
||||
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<C>(self, consumer: C) -> C::Result
|
||||
where
|
||||
C: UnindexedConsumer<Self::Item>,
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
entries: Vec<T>,
|
||||
free_send: Sender<usize>,
|
||||
free_recv: Receiver<usize>,
|
||||
}
|
||||
|
||||
impl<T> RcSlab<T> {
|
||||
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<Item = &T> + Clone + '_ {
|
||||
self.entries.iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl FusedIterator<Item = &mut T> + '_ {
|
||||
self.entries.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Key(Arc<KeyInner>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct KeyInner {
|
||||
index: usize,
|
||||
free_send: Sender<usize>,
|
||||
}
|
||||
|
||||
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<Ordering> {
|
||||
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<H: Hasher>(&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);
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
slab: Slab<Slot<T>>,
|
||||
version: NonZeroU32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Slot<T> {
|
||||
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<T> VersionedSlab<T> {
|
||||
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<T> {
|
||||
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<Item = (Key, &T)> + 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<Item = (Key, &mut T)> + 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<Item = (Key, &T)> + 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<Item = (Key, &mut T)> + '_
|
||||
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);
|
||||
}
|
||||
}
|
136
crates/valence/src/unit_test/example.rs
Normal file
136
crates/valence/src/unit_test/example.rs
Normal file
|
@ -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::<Server>();
|
||||
let tick = server.current_tick();
|
||||
app.update();
|
||||
let server = app.world.resource::<Server>();
|
||||
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>(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(())
|
||||
}
|
||||
}
|
2
crates/valence/src/unit_test/mod.rs
Normal file
2
crates/valence/src/unit_test/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
mod example;
|
||||
pub(crate) mod util;
|
204
crates/valence/src/unit_test/util.rs
Normal file
204
crates/valence/src/unit_test/util.rs
Normal file
|
@ -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<Mutex<MockClientBuffers>>,
|
||||
}
|
||||
|
||||
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<BytesMut> {
|
||||
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<Vec<S2cPlayPacket<'a>>> {
|
||||
self.dec.queue_bytes(self.conn.take_sent());
|
||||
|
||||
self.dec.collect_into_vec::<S2cPlayPacket<'a>>()
|
||||
}
|
||||
|
||||
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::<Server>();
|
||||
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<valence_protocol::packets::S2cPlayPacket> = &$sent_packets;
|
||||
let positions = [
|
||||
$((sent_packets.iter().position(|p| matches!(p, $packets))),)*
|
||||
];
|
||||
assert!(positions.windows(2).all(|w: &[Option<usize>]| w[0] < w[1]));
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_packet_count {
|
||||
($sent_packets:ident, $count:tt, $packet:pat) => {{
|
||||
let sent_packets: &Vec<valence_protocol::packets::S2cPlayPacket> = &$sent_packets;
|
||||
let count = sent_packets.iter().filter(|p| matches!(p, $packet)).count();
|
||||
assert_eq!(
|
||||
count,
|
||||
$count,
|
||||
"expected {} {} packets, got {}",
|
||||
$count,
|
||||
stringify!($packet),
|
||||
count
|
||||
);
|
||||
}};
|
||||
}
|
|
@ -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<T>(bottom: Vec3<T>, size: Vec3<T>) -> Aabb<T>
|
||||
where
|
||||
T: Float + 'static,
|
||||
f64: AsPrimitive<T>,
|
||||
{
|
||||
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, 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<f64> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
193
crates/valence/src/view.rs
Normal file
193
crates/valence/src/view.rs
Normal file
|
@ -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<ChunkPos> 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<ChunkPos> 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<ChunkPos>, dist: u8) -> Self {
|
||||
Self {
|
||||
pos: pos.into(),
|
||||
dist,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_pos(mut self, pos: impl Into<ChunkPos>) -> 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<Item = ChunkPos> {
|
||||
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<Item = ChunkPos> {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<C: Config> {
|
||||
slab: VersionedSlab<World<C>>,
|
||||
shared: SharedServer<C>,
|
||||
}
|
||||
|
||||
/// 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<C: Config> Worlds<C> {
|
||||
pub(crate) fn new(shared: SharedServer<C>) -> 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<C>) {
|
||||
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<C::WorldState> {
|
||||
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<C>) -> 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<C>> {
|
||||
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<C>> {
|
||||
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<Item = (WorldId, &World<C>)> + 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<Item = (WorldId, &mut World<C>)> + 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<Item = (WorldId, &World<C>)> + 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<Item = (WorldId, &mut World<C>)> + '_ {
|
||||
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<C: Config> Index<WorldId> for Worlds<C> {
|
||||
type Output = World<C>;
|
||||
|
||||
fn index(&self, index: WorldId) -> &Self::Output {
|
||||
self.get(index).expect("invalid world ID")
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Config> IndexMut<WorldId> for Worlds<C> {
|
||||
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<C: Config> {
|
||||
/// Custom state.
|
||||
pub state: C::WorldState,
|
||||
pub chunks: Chunks<C>,
|
||||
dimension: DimensionId,
|
||||
deleted: bool,
|
||||
}
|
||||
|
||||
impl<C: Config> Deref for World<C> {
|
||||
type Target = C::WorldState;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Config> DerefMut for World<C> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Config> World<C> {
|
||||
/// 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;
|
||||
}
|
||||
}
|
|
@ -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"]
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
208
crates/valence_anvil/examples/anvil_loading.rs
Normal file
208
crates/valence_anvil/examples/anvil_loading.rs
Normal file
|
@ -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<ChunkPos, Option<Priority>>,
|
||||
sender: Sender<ChunkPos>,
|
||||
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::<Server>()
|
||||
.new_instance(DimensionId::default());
|
||||
|
||||
world.spawn(instance);
|
||||
}
|
||||
|
||||
fn init_clients(
|
||||
mut clients: Query<&mut Client, Added<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
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<GameState>,
|
||||
) {
|
||||
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<GameState>) {
|
||||
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<ChunkPos>,
|
||||
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<Option<Chunk>> {
|
||||
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))
|
||||
}
|
|
@ -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<PlayerListId>;
|
||||
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<Self>,
|
||||
_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<Self>) {
|
||||
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<Self>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<C, F>(
|
||||
/// [`AnvilWorld::read_chunk`]: crate::AnvilWorld::read_chunk
|
||||
pub fn to_valence<F, const LOADED: bool>(
|
||||
nbt: &Compound,
|
||||
chunk: &mut C,
|
||||
chunk: &mut Chunk<LOADED>,
|
||||
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 {
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Option<P>>
|
||||
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<Vec<P>>
|
||||
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<bool> {
|
||||
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::<TestPacket>().unwrap();
|
||||
|
||||
assert_eq!(packets, res);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Self> {
|
||||
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<T> {
|
|||
|
||||
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<Self> {
|
||||
<&B>::decode(r).map(Cow::Borrowed)
|
||||
B::Owned::decode(r).map(Cow::Owned)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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<SignedProperty<'a>>,
|
||||
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<Text>,
|
||||
pub chat_type_name: Cow<'a, Text>,
|
||||
pub target_name: Option<Cow<'a, Text>>,
|
||||
}
|
||||
|
||||
#[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<Uuid>,
|
||||
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<Text>,
|
||||
pub prompt_message: Option<Cow<'a, Text>>,
|
||||
}
|
||||
|
||||
#[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<Text>,
|
||||
pub motd: Option<Cow<'a, Text>>,
|
||||
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,
|
||||
|
|
|
@ -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<Vec<Icon>>,
|
||||
pub icons: Option<Vec<Icon<'a>>>,
|
||||
pub data: Option<Data<'a>>,
|
||||
}
|
||||
|
||||
#[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<Text>,
|
||||
pub display_name: Option<Cow<'a, Text>>,
|
||||
}
|
||||
|
||||
#[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 = <Option<Vec<Icon>>>::decode(r)?;
|
||||
let icons = <Option<Vec<Icon<'a>>>>::decode(r)?;
|
||||
let columns = u8::decode(r)?;
|
||||
|
||||
let data = if columns > 0 {
|
||||
|
|
|
@ -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<MessageSignature<'a>>,
|
||||
pub unsigned_content: Option<Text>,
|
||||
pub unsigned_content: Option<Cow<'a, Text>>,
|
||||
pub filter_type: MessageFilterType,
|
||||
pub filter_type_bits: Option<u8>,
|
||||
pub chat_type: VarInt,
|
||||
pub network_name: Text,
|
||||
pub network_target_name: Option<Text>,
|
||||
pub network_name: Cow<'a, Text>,
|
||||
pub network_target_name: Option<Cow<'a, Text>>,
|
||||
}
|
||||
|
||||
#[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::<MessageSignature>::decode(r)?;
|
||||
let unsigned_content = Option::<Text>::decode(r)?;
|
||||
let unsigned_content = Option::<Cow<'a, Text>>::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::<Text>::decode(r)?;
|
||||
let network_name = <Cow<'a, Text>>::decode(r)?;
|
||||
let network_target_name = Option::<Cow<'a, Text>>::decode(r)?;
|
||||
|
||||
Ok(Self {
|
||||
sender,
|
||||
|
|
|
@ -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<Entry<'a>>,
|
||||
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<SignedProperty<'a>>,
|
||||
pub properties: Cow<'a, [Property]>,
|
||||
pub chat_data: Option<ChatData<'a>>,
|
||||
pub listed: bool,
|
||||
pub ping: i32,
|
||||
pub game_mode: GameMode,
|
||||
pub display_name: Option<Text>,
|
||||
pub display_name: Option<Cow<'a, Text>>,
|
||||
}
|
||||
|
||||
#[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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ItemStack>,
|
||||
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<Self> {
|
||||
let title = Text::decode(r)?;
|
||||
let description = Text::decode(r)?;
|
||||
let title = <Cow<'a, Text>>::decode(r)?;
|
||||
let description = <Cow<'a, Text>>::decode(r)?;
|
||||
let icon = Option::<ItemStack>::decode(r)?;
|
||||
let frame_type = VarInt::decode(r)?;
|
||||
let flags = i32::decode(r)?;
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -817,6 +817,18 @@ impl From<bool> for Text {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Text> 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)
|
||||
|
|
|
@ -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<String>,
|
||||
pub struct Property<S = String> {
|
||||
pub name: S,
|
||||
pub value: S,
|
||||
pub signature: Option<S>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
|
|
@ -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<S> From<Username<S>> for Text
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
fn from(value: Username<S>) -> Self {
|
||||
Text::text(value.as_str().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> fmt::Display for Username<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "valence_derive"
|
||||
name = "valence_protocol_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue