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:
Ryan Johnson 2023-02-11 09:51:53 -08:00 committed by GitHub
parent 44ea6915db
commit cb9230ec34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 9842 additions and 10888 deletions

55
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View 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
View 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.

View 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
View 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. -->

View file

@ -24,6 +24,7 @@ jobs:
with: with:
components: clippy, rustfmt components: clippy, rustfmt
- run: cp crates/playground/src/playground.template.rs crates/playground/src/playground.rs
- name: Validate formatting - name: Validate formatting
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
- name: Validate documentation - name: Validate documentation

4
.gitignore vendored
View file

@ -8,9 +8,9 @@ Cargo.lock
/extractor/out /extractor/out
/extractor/classes /extractor/classes
/extractor/run /extractor/run
/crates/*/rust-mc-bot rust-mc-bot
.asset_cache/ .asset_cache/
/velocity /velocity
flamegraph.svg flamegraph*.svg
perf.data perf.data
perf.data.old perf.data.old

View file

@ -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 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. 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 # Automatic Checks
When you submit a pull request, your code will automatically run through clippy, rustfmt, etc. to check for any errors. 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. 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) Consider using [intra-doc links](https://doc.rust-lang.org/rustdoc/write-documentation/linking-to-items-by-name.html)
where appropriate. 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.

View file

@ -1,6 +1,6 @@
[workspace] [workspace]
members = ["crates/*"] members = ["crates/*"]
exclude = ["crates/*/rust-mc-bot"] exclude = ["rust-mc-bot"]
[profile.dev.package."*"] [profile.dev.package."*"]
opt-level = 3 opt-level = 3

101
README.md
View file

@ -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. 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 Built on top of [Bevy ECS](https://bevyengine.org/learn/book/getting-started/ecs/), Valence is an effort to create a
completely from scratch in Rust. The difference is that Valence has decided to organize the effort a little differently. Minecraft compatible server completely from scratch in Rust. You can think of Valence as a _game engine for
All game logic is behind a trait. This approach has many advantages. Features such as a plugin system, dedicated Minecraft servers_. It doesn't do much by default, but by writing game logic yourself and leveraging Bevy's
executable, and vanilla game mechanics can be implemented _on top of_ Valence. Valence is a Rust library like any other. 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 Opinionated features like dynamic scripting, dedicated executables, and vanilla game mechanics are all expected to be
like a minigame server without need for vanilla game mechanics, you can depend on Valence directly. 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 # Goals
Valence aims to be the following: Valence aims to be the following:
* **Complete**. Abstractions for the full breadth of the Minecraft protocol. * **Complete**. Abstractions for the full breadth of the Minecraft protocol.
* **Flexible**. Valence provides direct access to Minecraft's protocol when necessary. * **Flexible**. Valence provides direct access to the lowest levels of Minecraft's protocol when necessary.
* **Minimal**. The API surface is small with only the necessities exposed. Opinionated features such as a * **Modular**. Pick and choose the features you actually need.
standalone executable, plugin system, and reimplementation of vanilla mechanics should be built in a separate project on * **Intuitive**. An API that is easy to use and difficult to misuse. Extensive documentation and examples are important.
top of the foundation that Valence provides. * **Efficient**. Optimal use of system resources with multiple CPU cores in mind. Valence uses very little memory and
* **Intuitive**. An API that is easy to use and difficult to misuse. Extensive documentation is important. can
* **Efficient**. Optimal use of system resources with multiple CPU cores in mind. 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 * **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 ## Current Status
Valence is still early in development with many features unimplemented or incomplete. However, the foundations are in Valence is still early in development with many features unimplemented or incomplete. However, the foundations are in
place. Here are some noteworthy achievements: place. Here are some noteworthy achievements:
- [x] A new serde library for Minecraft's Named Binary Tag (NBT) format - `valence_nbt`: A speedy new library for Minecraft's Named Binary Tag (NBT) format.
- [x] Authentication, encryption, and compression - `valence_protocol`: A library for working with Minecraft's protocol. Does not depend on Valence and can be used in
- [x] Block states other projects.
- [x] Chunks - Authentication, encryption, and compression
- [x] Entities and tracked data - Block states
- [x] Bounding volume hierarchy for fast spatial entity queries - Chunks
- [x] Player list and player skins - Entities and metadata
- [x] Dimensions, biomes, and worlds - Bounding volume hierarchy for fast spatial entity queries
- [x] JSON Text API - Player list and player skins
- [x] A Fabric mod for extracting data from the game into JSON files. These files are processed by a build script to - 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. generate Rust code for the project. The JSON files can be used in other projects as well.
- [x] Items - Inventories
- [x] Particles - Items
- [x] Anvil file format (read only) - Particles
- [ ] Inventory - Anvil file format (read only)
- [ ] Block entities - Proxy support ([Velocity](https://velocitypowered.com/), [Bungeecord](https://www.spigotmc.org/wiki/bungeecord/)
- [x] Proxy support ([Velocity](https://velocitypowered.com/), [Bungeecord](https://www.spigotmc.org/wiki/bungeecord/) and [Waterfall](https://docs.papermc.io/waterfall)) and [Waterfall](https://docs.papermc.io/waterfall))
- [ ] Utilities for continuous collision detection
Here is a [short video](https://www.youtube.com/watch?v=6P072lKE01s) showing the examples and some of its current Here is a [short video](https://www.youtube.com/watch?v=6P072lKE01s) (outdated) showing the examples and some of
capabilities. Valence's capabilities.
# Getting Started # Getting Started
## Running the Examples ## Running the Examples
You may want to try running one of the examples. After cloning the repository, run After cloning the repository, run
```shell ```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`. Next, open your Minecraft client and connect to the address `localhost`.
If all goes well you should be playing on the server. 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 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) [git dependency](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories)
.
```toml ```toml
[dependencies] [dependencies]
valence = { git = "https://github.com/valence-rs/valence" } 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 # Contributing
@ -97,8 +114,8 @@ under [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/)
# Funding # 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) 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! I would love to continue working on Valence and your support would help me do that. Thanks!

View file

@ -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.

View file

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

View file

@ -1,9 +1,9 @@
use std::error::Error; use std::error::Error;
use std::fmt::Write; use std::fmt::Write;
use std::io;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use std::{fmt, io};
use anyhow::bail; use anyhow::bail;
use clap::Parser; use clap::Parser;
@ -63,7 +63,7 @@ struct State {
impl State { impl State {
pub async fn rw_packet<'a, P>(&'a mut self) -> anyhow::Result<P> pub async fn rw_packet<'a, P>(&'a mut self) -> anyhow::Result<P>
where where
P: DecodePacket<'a> + EncodePacket + fmt::Debug, P: DecodePacket<'a> + EncodePacket,
{ {
while !self.dec.has_next_packet()? { while !self.dec.has_next_packet()? {
self.dec.reserve(4096); self.dec.reserve(4096);

View file

@ -1,8 +1,11 @@
[package] [package]
name = "bench_players" name = "playground"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.65"
glam = "0.22.0"
tracing = "0.1.37"
tracing-subscriber = "0.3.16" tracing-subscriber = "0.3.16"
valence = { path = "../valence" } valence = { path = "../valence" }

View 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
View file

@ -0,0 +1 @@
playground.rs

View file

@ -0,0 +1,2 @@
//! Put stuff in here if you find that you have to write the same code for
//! multiple playgrounds.

View 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();
}

View 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!

View file

@ -6,7 +6,7 @@ description = "A framework for building Minecraft servers in Rust."
repository = "https://github.com/rj00a/valence" repository = "https://github.com/rj00a/valence"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
keywords = ["minecraft", "gamedev", "server"] keywords = ["minecraft", "gamedev", "server", "ecs"]
categories = ["game-engines"] categories = ["game-engines"]
build = "build/main.rs" build = "build/main.rs"
authors = ["Ryan Johnson <ryanj00a@gmail.com>"] authors = ["Ryan Johnson <ryanj00a@gmail.com>"]
@ -14,18 +14,20 @@ authors = ["Ryan Johnson <ryanj00a@gmail.com>"]
[dependencies] [dependencies]
anyhow = "1.0.65" anyhow = "1.0.65"
arrayvec = "0.7.2" arrayvec = "0.7.2"
async-trait = "0.1.57" async-trait = "0.1.60"
base64 = "0.13.0" base64 = "0.21.0"
bitfield-struct = "0.1.8" bevy_app = "0.9.1"
bevy_ecs = "0.9.1"
bitfield-struct = "0.3.1"
bytes = "1.2.1" bytes = "1.2.1"
flume = "0.10.14" flume = "0.10.14"
futures = "0.3.24" glam = "0.22.0"
hmac = "0.12.1" hmac = "0.12.1"
num = "0.4.0" num = "0.4.0"
paste = "1.0.9" parking_lot = "0.12.1"
paste = "1.0.11"
rand = "0.8.5" rand = "0.8.5"
rayon = "1.5.3" rsa = "0.7.2"
rsa = "0.6.1"
rsa-der = "0.3.0" rsa-der = "0.3.0"
rustc-hash = "1.1.0" rustc-hash = "1.1.0"
serde = { version = "1.0.145", features = ["derive"] } serde = { version = "1.0.145", features = ["derive"] }
@ -33,16 +35,12 @@ serde_json = "1.0.85"
sha1 = "0.10.5" sha1 = "0.10.5"
sha2 = "0.10.6" sha2 = "0.10.6"
thiserror = "1.0.35" thiserror = "1.0.35"
tokio = { version = "1.25.0", features = ["full"] }
tracing = "0.1.37" tracing = "0.1.37"
url = { version = "2.2.2", features = ["serde"] } url = { version = "2.2.2", features = ["serde"] }
uuid = { version = "1.1.2", features = ["serde"] } uuid = { version = "1.1.2", features = ["serde"] }
valence_nbt = { version = "0.5.0", path = "../valence_nbt" } valence_nbt = { version = "0.5.0", path = "../valence_nbt" }
valence_protocol = { version = "0.1.0", path = "../valence_protocol", features = ["encryption", "compression"] } 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] [dependencies.reqwest]
version = "0.11.12" version = "0.11.12"
@ -52,9 +50,9 @@ features = ["rustls-tls", "json"]
[dev-dependencies] [dev-dependencies]
approx = "0.5.1" approx = "0.5.1"
tracing-subscriber = "0.3.16" glam = { version = "0.22.0", features = ["approx"] }
noise = "0.8.2" noise = "0.8.2"
valence_spatial_index = { path = "../valence_spatial_index", version = "0.1.0" } tracing-subscriber = "0.3.16"
[build-dependencies] [build-dependencies]
anyhow = "1.0.65" anyhow = "1.0.65"

View file

@ -128,7 +128,7 @@ impl Value {
Value::Facing(_) => quote!(Facing), Value::Facing(_) => quote!(Facing),
Value::OptionalUuid(_) => quote!(Option<Uuid>), Value::OptionalUuid(_) => quote!(Option<Uuid>),
Value::OptionalBlockState(_) => quote!(BlockState), Value::OptionalBlockState(_) => quote!(BlockState),
Value::NbtCompound(_) => quote!(crate::nbt::Compound), Value::NbtCompound(_) => quote!(valence_nbt::Compound),
Value::Particle(_) => quote!(Particle), Value::Particle(_) => quote!(Particle),
Value::VillagerData { .. } => quote!(VillagerData), Value::VillagerData { .. } => quote!(VillagerData),
Value::OptionalInt(_) => quote!(OptionalInt), Value::OptionalInt(_) => quote!(OptionalInt),
@ -145,7 +145,7 @@ impl Value {
Value::String(_) => quote!(&str), Value::String(_) => quote!(&str),
Value::TextComponent(_) => quote!(&Text), Value::TextComponent(_) => quote!(&Text),
Value::OptionalTextComponent(_) => quote!(Option<&Text>), Value::OptionalTextComponent(_) => quote!(Option<&Text>),
Value::NbtCompound(_) => quote!(&crate::nbt::Compound), Value::NbtCompound(_) => quote!(&valence_nbt::Compound),
_ => self.field_type(), _ => self.field_type(),
} }
} }
@ -191,7 +191,7 @@ impl Value {
} }
Value::OptionalUuid(_) => quote!(None), // TODO Value::OptionalUuid(_) => quote!(None), // TODO
Value::OptionalBlockState(_) => quote!(BlockState::default()), // 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) => { Value::Particle(p) => {
let variant = ident(p.to_pascal_case()); let variant = ident(p.to_pascal_case());
quote!(Particle::#variant) quote!(Particle::#variant)

View file

@ -8,13 +8,13 @@ use serde::Deserialize;
use crate::ident; use crate::ident;
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
struct EntityData { struct EntityEvents {
statuses: BTreeMap<String, u8>, statuses: BTreeMap<String, u8>,
animations: BTreeMap<String, u8>, animations: BTreeMap<String, u8>,
} }
pub fn build() -> anyhow::Result<TokenStream> { pub fn build() -> anyhow::Result<TokenStream> {
let entity_data: EntityData = let entity_data: EntityEvents =
serde_json::from_str(include_str!("../../../extracted/entity_data.json"))?; serde_json::from_str(include_str!("../../../extracted/entity_data.json"))?;
let mut statuses: Vec<_> = entity_data.statuses.into_iter().collect(); 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(); let mut animations: Vec<_> = entity_data.animations.into_iter().collect();
animations.sort_by_key(|(_, id)| *id); animations.sort_by_key(|(_, id)| *id);
let event_variants = statuses let entity_status_variants: Vec<_> = statuses
.iter() .iter()
.chain(animations.iter()) .map(|(name, code)| {
.map(|(name, _)| ident(name.to_pascal_case())); let name = ident(name.to_pascal_case());
let code = *code as isize;
let status_arms = statuses.iter().map(|(name, code)| { quote! {
let name = ident(name.to_pascal_case()); #name = #code,
quote! { }
Self::#name => StatusOrAnimation::Status(#code), })
} .collect();
});
let animation_arms = animations.iter().map(|(name, code)| { let entity_animation_variants: Vec<_> = animations
let name = ident(name.to_pascal_case()); .iter()
quote! { .map(|(name, code)| {
Self::#name => StatusOrAnimation::Animation(#code), let name = ident(name.to_pascal_case());
} let code = *code as isize;
});
quote! {
#name = #code,
}
})
.collect();
Ok(quote! { Ok(quote! {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum EntityEvent { pub enum EntityStatus {
#(#event_variants,)* #(#entity_status_variants)*
} }
impl EntityEvent { #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) fn status_or_animation(self) -> StatusOrAnimation { pub enum EntityAnimation {
match self { #(#entity_animation_variants)*
#(#status_arms)*
#(#animation_arms)*
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum StatusOrAnimation {
Status(u8),
Animation(u8),
} }
}) })
} }

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

View file

@ -1,208 +1,89 @@
use std::iter; use valence::client::despawn_disconnected_clients;
use std::net::SocketAddr; use valence::client::event::default_event_handler;
use std::sync::atomic::{AtomicUsize, Ordering};
use valence::prelude::*; use valence::prelude::*;
pub fn main() -> ShutdownResult { const SPAWN_Y: i32 = 0;
const BIOME_COUNT: usize = 10;
pub fn main() {
tracing_subscriber::fmt().init(); tracing_subscriber::fmt().init();
valence::start_server( App::new()
Game { .add_plugin(
player_count: AtomicUsize::new(0), ServerPlugin::new(()).with_biomes(
}, (1..BIOME_COUNT)
ServerState { player_list: None }, .map(|i| {
) let color = (0xffffff / BIOME_COUNT * i) as u32;
} Biome {
name: ident!("valence:test_biome_{i}"),
struct Game { sky_color: color,
player_count: AtomicUsize, water_fog_color: color,
} fog_color: color,
water_color: color,
struct ServerState { foliage_color: Some(color),
player_list: Option<PlayerListId>, grass_color: Some(color),
} ..Default::default()
#[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);
} }
}
// 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() .chain(std::iter::once(Biome {
{ name: ident!("plains"),
client.disconnect("The server is full!".color(Color::RED)); ..Default::default()
return false; }))
} .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 fn setup(world: &mut World) {
.entities let server = world.resource::<Server>();
.insert_with_uuid(EntityKind::Player, client.uuid(), ()) let mut instance = server.new_instance(DimensionId::default());
{
Some((id, entity)) => { for z in -5..5 {
entity.set_world(world_id); for x in -5..5 {
client.entity_id = id let mut chunk = Chunk::new(4);
} // Set chunk blocks
None => { for z in 0..16 {
client.disconnect("Conflicting UUID"); for x in 0..16 {
return false; 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() { fn init_clients(
self.player_count.fetch_sub(1, Ordering::SeqCst); mut clients: Query<&mut Client, Added<Client>>,
server.entities[client.entity_id].set_deleted(true); instances: Query<Entity, With<Instance>>,
if let Some(id) = &server.state.player_list { ) {
server.player_lists[id].remove(client.uuid()); for mut client in &mut clients {
} client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
return false; client.set_respawn_screen(true);
} client.set_instance(instances.single());
client.set_game_mode(GameMode::Creative);
if client.position().y < MIN_Y as _ { client.send_message("Welcome to Valence!".italic());
client.teleport(spawn_pos, client.yaw(), client.pitch());
}
true
});
} }
} }

View file

@ -1,201 +1,152 @@
use std::net::SocketAddr; use valence::client::despawn_disconnected_clients;
use std::sync::atomic::{AtomicUsize, Ordering}; use valence::client::event::{
default_event_handler, FinishDigging, StartDigging, StartSneaking, UseItemOnBlock,
};
use valence::prelude::*; use valence::prelude::*;
use valence_protocol::types::Hand;
pub fn main() -> ShutdownResult { const SPAWN_Y: i32 = 64;
pub fn main() {
tracing_subscriber::fmt().init(); tracing_subscriber::fmt().init();
valence::start_server( App::new()
Game { .add_plugin(ServerPlugin::new(()))
player_count: AtomicUsize::new(0), .add_system_to_stage(EventLoop, default_event_handler)
}, .add_system_to_stage(EventLoop, toggle_gamemode_on_sneak)
ServerState { player_list: None }, .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 { fn setup(world: &mut World) {
player_count: AtomicUsize, let mut instance = world
} .resource::<Server>()
.new_instance(DimensionId::default());
struct ServerState { for z in -5..5 {
player_list: Option<PlayerListId>, for x in -5..5 {
} instance.insert_chunk([x, z], Chunk::default());
#[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(),
),
} }
} }
fn init(&self, server: &mut Server<Self>) { for z in -25..25 {
let world = server.worlds.insert(DimensionId::default(), ()).1; for x in -25..25 {
server.state.player_list = Some(server.player_lists.insert(()).0); instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
// 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);
}
} }
} }
fn update(&self, server: &mut Server<Self>) { world.spawn(instance);
let (world_id, world) = server.worlds.iter_mut().next().unwrap(); }
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| { fn toggle_gamemode_on_sneak(
if client.created_this_tick() { mut clients: Query<&mut Client>,
if self mut events: EventReader<StartSneaking>,
.player_count ) {
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { for event in events.iter() {
(count < MAX_PLAYERS).then_some(count + 1) let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else {
}) continue;
.is_err() };
{ let mode = client.game_mode();
client.disconnect("The server is full!".color(Color::RED)); client.set_game_mode(match mode {
return false; GameMode::Survival => GameMode::Creative,
} GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
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 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());
}
}

View file

@ -1,268 +1,100 @@
pub fn main() { use tracing::warn;
todo!("reimplement when inventories are re-added"); use valence::client::despawn_disconnected_clients;
} use valence::client::event::{default_event_handler, StartSneaking, UseItemOnBlock};
/*
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use num::Integer;
use valence::prelude::*; 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(); tracing_subscriber::fmt().init();
valence::start_server( App::new()
Game { .add_plugin(ServerPlugin::new(()))
player_count: AtomicUsize::new(0), .add_system_to_stage(EventLoop, default_event_handler)
}, .add_system_to_stage(EventLoop, toggle_gamemode_on_sneak)
ServerState { .add_system_to_stage(EventLoop, open_chest)
player_list: None, .add_system_set(PlayerList::default_system_set())
chest: Default::default(), .add_startup_system(setup)
tick: 0, .add_system(init_clients)
}, .add_system(despawn_disconnected_clients)
) .run();
} }
struct Game { fn setup(world: &mut World) {
player_count: AtomicUsize, let mut instance = world
} .resource::<Server>()
.new_instance(DimensionId::default());
struct ServerState { for z in -5..5 {
player_list: Option<PlayerListId>, for x in -5..5 {
chest: InventoryId, instance.insert_chunk([x, z], Chunk::default());
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()),
} }
} }
fn init(&self, server: &mut Server<Self>) { for z in -25..25 {
let world = server.worlds.insert(DimensionId::default(), ()).1; for x in -25..25 {
server.state.player_list = Some(server.player_lists.insert(()).0); instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
// 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 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>) { world.spawn(instance);
server.state.tick += 1;
if server.state.tick > 10 {
server.state.tick = 0;
}
let (world_id, world) = server.worlds.iter_mut().next().unwrap();
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) { fn init_clients(
if server.state.tick == 0 { mut clients: Query<&mut Client, Added<Client>>,
rotate_items(inv); 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| { fn toggle_gamemode_on_sneak(
if client.created_this_tick() { mut clients: Query<&mut Client>,
if self mut events: EventReader<StartSneaking>,
.player_count ) {
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { for event in events.iter() {
(count < MAX_PLAYERS).then_some(count + 1) let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else {
}) continue;
.is_err() };
{ let mode = client.game_mode();
client.disconnect("The server is full!".color(Color::RED)); client.set_game_mode(match mode {
return false; GameMode::Survival => GameMode::Creative,
} GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
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 rotate_items(inv: &mut ConfigurableInventory) { fn open_chest(
for i in 1..inv.slot_count() { mut commands: Commands,
let a = inv.slot((i - 1) as SlotId); inventories: Query<Entity, (With<Inventory>, Without<Client>)>,
let b = inv.set_slot(i as SlotId, a.cloned()); mut events: EventReader<UseItemOnBlock>,
inv.set_slot((i - 1) as SlotId, b); ) {
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);
} }
} }
*/

View file

@ -1,252 +1,166 @@
use std::net::SocketAddr; use glam::Vec3Swizzles;
use std::sync::atomic::{AtomicUsize, Ordering}; use valence::client::despawn_disconnected_clients;
use valence::client::event::{
default_event_handler, InteractWithEntity, StartSprinting, StopSprinting,
};
use valence::prelude::*; 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(); tracing_subscriber::fmt().init();
valence::start_server( App::new()
Game { .add_plugin(ServerPlugin::new(()))
player_count: AtomicUsize::new(0), .add_startup_system(setup)
}, .add_system_to_stage(EventLoop, default_event_handler)
None, .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 { fn setup(world: &mut World) {
player_count: AtomicUsize, let mut instance = world
} .resource::<Server>()
.new_instance(DimensionId::default());
#[derive(Default)] for z in -5..5 {
struct ClientState { for x in -5..5 {
/// The client's player entity. instance.insert_chunk([x, z], Chunk::default());
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(),
),
} }
} }
fn init(&self, server: &mut Server<Self>) { // Create circular arena.
let (_, world) = server.worlds.insert(DimensionId::default(), ()); for z in -ARENA_RADIUS..ARENA_RADIUS {
server.state = Some(server.player_lists.insert(()).0); 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(); if dist > 1.0 {
let height = world.chunks.height(); continue;
}
// Create circular arena. let block = if rand::random::<f64>() < dist {
let size = 2; BlockState::STONE
for chunk_z in -size - 2..size + 2 { } else {
for chunk_x in -size - 2..size + 2 { BlockState::DEEPSLATE
let mut chunk = UnloadedChunk::new(height); };
let r = -size..size; for y in 0..SPAWN_Y {
if r.contains(&chunk_x) && r.contains(&chunk_z) { instance.set_block_state([x, y, z], block);
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, ());
} }
} }
world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK);
} }
fn update(&self, server: &mut Server<Self>) { world.spawn(instance);
let current_tick = server.current_tick(); }
let (world_id, _) = server.worlds.iter_mut().next().unwrap();
server.clients.retain(|client_id, client| { fn init_clients(
if client.created_this_tick() { mut commands: Commands,
if self mut clients: Query<(Entity, &mut Client), Added<Client>>,
.player_count instances: Query<Entity, With<Instance>>,
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { ) {
(count < MAX_PLAYERS).then_some(count + 1) let instance = instances.single();
})
.is_err()
{
client.disconnect("The server is full!".color(Color::RED));
return false;
}
let (player_id, player) = match server.entities.insert_with_uuid( for (entity, mut client) in &mut clients {
EntityKind::Player, client.set_position([0.0, SPAWN_Y as f64, 0.0]);
client.uuid(), client.set_instance(instance);
EntityState::default(),
) {
Some(e) => e,
None => {
client.disconnect("Conflicting UUID");
return false;
}
};
player.set_world(world_id); commands.entity(entity).insert((
player.client = client_id; 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); for &StopSprinting { client } in stop_sprinting.iter() {
client.set_flat(true); if let Ok((_, mut state, _)) = clients.get_mut(client) {
client.set_game_mode(GameMode::Survival); state.has_bonus_knockback = false;
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());
if let Some(id) = &server.state { for &InteractWithEntity {
server.player_lists[id].insert( client: attacker_client,
client.uuid(), entity_id,
client.username(), ..
client.textures().cloned(), } in interact_with_entity.iter()
client.game_mode(), {
0, let Some(victim_client) = manager.get_with_protocol_id(entity_id) else {
None, // Attacked entity doesn't exist.
true, continue
); };
}
client.send_message("Welcome to the arena.".italic()); let Ok([(attacker_client, mut attacker_state, _), (mut victim_client, mut victim_state, mut victim_entity)]) =
if self.player_count.load(Ordering::SeqCst) <= 1 { clients.get_many_mut([attacker_client, victim_client])
client.send_message("Have another player join the game with you.".italic()); else {
} // Victim or attacker does not exist, or the attacker is attacking itself.
} continue
};
while let Some(event) = client.next_event() { if server.current_tick() - victim_state.last_attacked_tick < 10 {
let player = server // Victim is still on attack cooldown.
.entities continue;
.get_mut(client.player) }
.expect("missing player entity");
event.handle_default(client, player); victim_state.last_attacked_tick = server.current_tick();
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;
client.extra_knockback = false; let victim_pos = victim_client.position().xz();
} let attacker_pos = attacker_client.position().xz();
}
}
_ => {}
}
}
if client.is_disconnected() { let dir = (victim_pos - attacker_pos).normalize().as_vec2();
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;
}
if client.position().y <= 0.0 { let knockback_xz = if attacker_state.has_bonus_knockback {
client.teleport( 18.0
[ } else {
SPAWN_POS.x as f64 + 0.5, 8.0
SPAWN_POS.y as f64 + 1.0, };
SPAWN_POS.z as f64 + 0.5, let knockback_y = if attacker_state.has_bonus_knockback {
], 8.432
client.yaw(), } else {
client.pitch(), 6.432
); };
}
true victim_client.set_velocity([dir.x * knockback_xz, knockback_y, dir.y * knockback_xz]);
});
for (_, entity) in server.entities.iter_mut() { attacker_state.has_bonus_knockback = false;
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);
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 }; fn teleport_oob_clients(mut clients: Query<&mut Client>) {
let knockback_y = if entity.extra_knockback { 8.432 } else { 6.432 }; for mut client in &mut clients {
if client.position().y < 0.0 {
let vel = Vec3::new(dir.x * knockback_xz, knockback_y, dir.y * knockback_xz); client.set_position([0.0, SPAWN_Y as _, 0.0]);
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);
}
}
} }
} }
} }

View file

@ -1,297 +1,206 @@
use std::mem; use std::mem;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use num::Integer; use valence::client::despawn_disconnected_clients;
use rayon::prelude::*; use valence::client::event::{default_event_handler, StartDigging, StartSneaking};
use valence::prelude::*; 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(); tracing_subscriber::fmt().init();
valence::start_server( App::new()
Game { .add_plugin(ServerPlugin::new(()).with_biomes(vec![Biome {
player_count: AtomicUsize::new(0), grass_color: Some(0x00ff00),
}, ..Default::default()
ServerState { }]))
player_list: None, .add_system_to_stage(EventLoop, default_event_handler)
paused: false, .add_system_set(PlayerList::default_system_set())
board: vec![false; SIZE_X * SIZE_Z].into_boxed_slice(), .add_startup_system(setup)
board_buf: vec![false; SIZE_X * SIZE_Z].into_boxed_slice(), .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 { fn setup(world: &mut World) {
player_count: AtomicUsize, 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 { fn init_clients(
player_list: Option<PlayerListId>, mut clients: Query<&mut Client, Added<Client>>,
paused: bool, 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: Box<[bool]>,
board_buf: Box<[bool]>, board_buf: Box<[bool]>,
} }
#[derive(Default)] impl LifeBoard {
struct ClientState { pub fn get(&self, x: i32, z: i32) -> bool {
entity_id: EntityId, 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; self.board[x + z * BOARD_SIZE_X]
} else {
const SIZE_X: usize = 100; false
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(),
),
} }
} }
fn init(&self, server: &mut Server<Self>) { pub fn set(&mut self, x: i32, z: i32, value: bool) {
let world = server.worlds.insert(DimensionId::default(), ()).1; if (BOARD_MIN_X..=BOARD_MAX_X).contains(&x) && (BOARD_MIN_Z..=BOARD_MAX_Z).contains(&z) {
server.state.player_list = Some(server.player_lists.insert(()).0); 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 { self.board[x + z * BOARD_SIZE_X] = value;
for chunk_x in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 {
world
.chunks
.insert([chunk_x, chunk_z], UnloadedChunk::default(), ());
}
} }
} }
fn update(&self, server: &mut Server<Self>) { pub fn update(&mut self) {
let current_tick = server.current_tick(); for (idx, cell) in self.board_buf.iter_mut().enumerate() {
let (world_id, world) = server.worlds.iter_mut().next().unwrap(); let x = (idx % BOARD_SIZE_X) as i32;
let z = (idx / BOARD_SIZE_X) as i32;
let spawn_pos = [ let mut live_neighbors = 0;
SIZE_X as f64 / 2.0,
BOARD_Y as f64 + 1.0,
SIZE_Z as f64 / 2.0,
];
server.clients.retain(|_, client| { for cz in z - 1..=z + 1 {
if client.created_this_tick() { for cx in x - 1..=x + 1 {
if self if !(cx == x && cz == z) {
.player_count let idx = cx.rem_euclid(BOARD_SIZE_X as i32) as usize
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { + cz.rem_euclid(BOARD_SIZE_Z as i32) as usize * BOARD_SIZE_X;
(count < MAX_PLAYERS).then_some(count + 1)
})
.is_err()
{
client.disconnect("The server is full!".color(Color::RED));
return false;
}
match server live_neighbors += self.board[idx] as i32;
.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.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(); let live = self.board[idx];
if live {
while let Some(event) = client.next_event() { *cell = (2..=3).contains(&live_neighbors);
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)
} else { } else {
"Playing".color(Color::GREEN) *cell = live_neighbors == 3;
}); }
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);
} }
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) { pub fn clear(&mut self) {
for chunk_z in 0..Integer::div_ceil(&SIZE_Z, &16) { self.board.fill(false);
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 { fn toggle_cell_on_dig(mut events: EventReader<StartDigging>, mut board: ResMut<LifeBoard>) {
let b = if server.state.board[cell_x + cell_z * SIZE_X] { for event in events.iter() {
BlockState::GRASS_BLOCK let (x, z) = (event.position.x, event.position.z);
} else {
BlockState::DIRT let live = board.get(x, z);
}; board.set(x, z, !live);
chunk.set_block_state(x, (BOARD_Y - min_y) as usize, z, b); }
} }
}
} 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();
}
}
}

View file

@ -1,222 +1,100 @@
use std::borrow::Cow;
use std::f64::consts::TAU; 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::*; 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(); tracing_subscriber::fmt().init();
valence::start_server( App::new()
Game { .add_plugin(ServerPlugin::new(()))
player_count: AtomicUsize::new(0), .add_system_to_stage(EventLoop, default_event_handler)
}, .add_startup_system(setup)
ServerState { .add_system(init_clients)
player_list: None, .add_system(update_sphere)
cows: vec![], .add_system(despawn_disconnected_clients)
}, .add_system_set(PlayerList::default_system_set())
) .run();
} }
struct Game { fn setup(world: &mut World) {
player_count: AtomicUsize, let mut instance = world
} .resource::<Server>()
.new_instance(DimensionId::default());
struct ServerState { for z in -5..5 {
player_list: Option<PlayerListId>, for x in -5..5 {
cows: Vec<EntityId>, instance.insert_chunk([x, z], Chunk::default());
}
#[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(),
),
} }
} }
fn init(&self, server: &mut Server<Self>) { instance.set_block_state(SPAWN_POS, BlockState::BEDROCK);
let (world_id, world) = server.worlds.insert(DimensionId::default(), ());
server.state.player_list = Some(server.player_lists.insert(()).0);
let size = 5; let instance_id = world.spawn(instance).id();
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); world.spawn_batch(
[0; SPHERE_AMOUNT].map(|_| (McEntity::new(SPHERE_KIND, instance_id), SpherePart)),
);
}
server.state.cows.extend((0..200).map(|_| { fn init_clients(
let (id, e) = server.entities.insert(EntityKind::Cow, ()); mut clients: Query<&mut Client, Added<Client>>,
e.set_world(world_id); instances: Query<Entity, With<Instance>>,
id ) {
})); 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>) { fn update_sphere(server: Res<Server>, mut parts: Query<&mut McEntity, With<SpherePart>>) {
let current_tick = server.current_tick(); let time = server.current_tick() as f64 / server.tps() as f64;
let (world_id, _) = server.worlds.iter_mut().next().expect("missing world");
server.clients.retain(|_, client| { let rot_angles = DVec3::new(0.2, 0.4, 0.6) * SPHERE_FREQ * time * TAU % TAU;
if client.created_this_tick() { let rot = DQuat::from_euler(EulerRot::XYZ, rot_angles.x, rot_angles.y, rot_angles.z);
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 let radius = lerp(
.entities SPHERE_MIN_RADIUS,
.insert_with_uuid(EntityKind::Player, client.uuid(), ()) SPHERE_MAX_RADIUS,
{ ((time * SPHERE_FREQ * TAU).sin() + 1.0) / 2.0,
Some((id, entity)) => { );
entity.set_world(world_id);
client.entity_id = id
}
None => {
client.disconnect("Conflicting UUID");
return false;
}
}
client.respawn(world_id); for (mut entity, p) in parts.iter_mut().zip(fibonacci_spiral(SPHERE_AMOUNT)) {
client.set_flat(true); debug_assert!(p.is_normalized());
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 { let dir = rot * p;
server.player_lists[id].insert( let (yaw, pitch) = to_yaw_and_pitch(dir.as_vec3());
client.uuid(),
client.username(),
client.textures().cloned(),
client.game_mode(),
0,
None,
true,
);
}
}
let entity = &mut server.entities[client.entity_id]; entity.set_position(SPHERE_CENTER + dir * radius);
entity.set_yaw(yaw);
if client.is_disconnected() { entity.set_head_yaw(yaw);
self.player_count.fetch_sub(1, Ordering::SeqCst); entity.set_pitch(pitch);
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);
}
} }
} }
/// Distributes N points on the surface of a unit sphere. /// 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; let golden_ratio = (1.0 + 5_f64.sqrt()) / 2.0;
(0..n).map(move |i| { (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. // Map from unit square to unit sphere.
let theta = x * TAU; let theta = x * TAU;
let phi = (1.0 - 2.0 * y).acos(); 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
}

View file

@ -1,332 +1,93 @@
use std::net::SocketAddr; use tracing::warn;
use std::sync::atomic::{AtomicUsize, Ordering}; use valence::client::despawn_disconnected_clients;
use valence::client::event::{default_event_handler, PerformRespawn, StartSneaking};
use valence::prelude::*; use valence::prelude::*;
pub fn main() -> ShutdownResult { const SPAWN_Y: i32 = 64;
pub fn main() {
tracing_subscriber::fmt().init(); tracing_subscriber::fmt().init();
valence::start_server( App::new()
Game { .add_plugin(ServerPlugin::new(()))
player_count: AtomicUsize::new(0), .add_system_to_stage(EventLoop, default_event_handler)
}, .add_system_to_stage(EventLoop, squat_and_die)
ServerState::default(), .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 { fn setup(world: &mut World) {
player_count: AtomicUsize, for block in [BlockState::GRASS_BLOCK, BlockState::DEEPSLATE] {
} let mut instance = world
.resource::<Server>()
.new_instance(DimensionId::default());
#[derive(Default)] for z in -5..5 {
struct ClientState { for x in -5..5 {
entity_id: EntityId, instance.insert_chunk([x, z], Chunk::default());
// 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(),
} }
}
fn init(&self, server: &mut Server<Self>) { for z in -25..25 {
// We created server with meaningless default state. for x in -25..25 {
// Let's create three worlds and create new ServerState. instance.set_block_state([x, SPAWN_Y, z], block);
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), world.spawn(instance);
third_world: create_world(server, THIRD_WORLD_SPAWN_BLOCK, WhichWorld::Third), }
}
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>) { client.kill(None, "Squatted too hard.");
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
});
} }
} }
// Boilerplate for creating world fn necromancy(
fn create_world(server: &mut Server<Game>, spawn_pos: BlockPos, world_type: WhichWorld) -> WorldId { mut clients: Query<&mut Client>,
let dimension = match world_type { mut events: EventReader<PerformRespawn>,
WhichWorld::First => server.shared.dimensions().next().unwrap(), instances: Query<Entity, With<Instance>>,
WhichWorld::Second => server.shared.dimensions().next().unwrap(), ) {
WhichWorld::Third => server.shared.dimensions().nth(1).unwrap(), for event in events.iter() {
}; let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else {
continue;
let (world_id, world) = server.worlds.insert(dimension.0, ()); };
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
// Create chunks client.set_velocity([0.0, 0.0, 0.0]);
for chunk_z in -3..3 { client.set_yaw(0.0);
for chunk_x in -3..3 { client.set_pitch(0.0);
world // make the client respawn in another instance
.chunks let idx = instances
.insert([chunk_x, chunk_z], UnloadedChunk::default(), ()); .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
} }

View file

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

View 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());
}
}
}

View file

@ -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,
});
}
}

View file

@ -1,45 +1,15 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use rand::Rng; use rand::Rng;
use valence::client::despawn_disconnected_clients;
use valence::client::event::default_event_handler;
use valence::prelude::*; 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 START_POS: BlockPos = BlockPos::new(0, 100, 0);
const VIEW_DIST: u8 = 10;
const BLOCK_TYPES: [BlockState; 7] = [ const BLOCK_TYPES: [BlockState; 7] = [
BlockState::GRASS_BLOCK, BlockState::GRASS_BLOCK,
@ -51,273 +21,217 @@ const BLOCK_TYPES: [BlockState; 7] = [
BlockState::MOSS_BLOCK, BlockState::MOSS_BLOCK,
]; ];
#[async_trait] pub fn main() {
impl Config for Game { tracing_subscriber::fmt().init();
type ServerState = Option<PlayerListId>;
type ClientState = ClientState;
type EntityState = ();
type WorldState = ();
type ChunkState = ChunkState;
type PlayerListState = ();
type InventoryState = ();
async fn server_list_ping( App::new()
&self, .add_plugin(ServerPlugin::new(()))
_server: &SharedServer<Self>, .add_system_to_stage(EventLoop, default_event_handler)
_remote_addr: SocketAddr, .add_system_set(PlayerList::default_system_set())
_protocol_version: i32, .add_system(init_clients)
) -> ServerListPing { .add_system(despawn_disconnected_clients)
ServerListPing::Respond { .add_system(reset_clients.after(init_clients))
online_players: self.player_count.load(Ordering::SeqCst) as i32, .add_system(manage_chunks.after(reset_clients).before(manage_blocks))
max_players: MAX_PLAYERS as i32, .add_system(manage_blocks)
player_sample: Default::default(), .run();
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
});
}
} }
fn reset(client: &mut Client<Game>, world: &mut World<Game>) { #[derive(Component)]
// Load chunks around spawn to avoid double void reset struct GameState {
for chunk_z in -1..3 { blocks: VecDeque<BlockPos>,
for chunk_x in -2..2 { score: u32,
world.chunks.insert( combo: u32,
[chunk_x, chunk_z], target_y: i32,
UnloadedChunk::default(), last_block_timestamp: u128,
ChunkState { keep_loaded: true }, }
);
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.set_position([
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(
[
START_POS.x as f64 + 0.5, START_POS.x as f64 + 0.5,
START_POS.y as f64 + 1.0, START_POS.y as f64 + 1.0,
START_POS.z as f64 + 0.5, START_POS.z as f64 + 0.5,
], ]);
0f32, client.set_flat(true);
0f32, 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) { fn reset_clients(
if in_game { mut clients: Query<(&mut Client, &mut GameState, &mut Instance), With<GameState>>,
let removed_block = client.blocks.pop_front().unwrap(); ) {
world.chunks.set_block_state(removed_block, BlockState::AIR); 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(); state.score = 0;
let block_pos = generate_random_block(last_pos, client.target_y); 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 { 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 { } 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(); let mut rng = rand::thread_rng();
world instance.set_block_state(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap());
.chunks state.blocks.push_back(block_pos);
.set_block_state(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap());
client.blocks.push_back(block_pos);
// Combo System // Combo System
client.last_block_timestamp = SystemTime::now() state.last_block_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
.as_millis(); .as_millis();
@ -333,9 +247,9 @@ fn generate_random_block(pos: BlockPos, target_y: i32) -> BlockPos {
_ => -1, _ => -1,
}; };
let z = match y { let z = match y {
1 => rng.gen_range(1..4), 1 => rng.gen_range(1..3),
-1 => rng.gen_range(2..6), -1 => rng.gen_range(2..5),
_ => rng.gen_range(1..5), _ => rng.gen_range(1..4),
}; };
let x = rng.gen_range(-3..4); let x = rng.gen_range(-3..4);

View file

@ -1,199 +1,99 @@
use std::fmt; use valence::client::despawn_disconnected_clients;
use std::net::SocketAddr; use valence::client::event::default_event_handler;
use std::sync::atomic::{AtomicUsize, Ordering};
use valence::prelude::*; 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(); tracing_subscriber::fmt().init();
valence::start_server( App::new()
Game { .add_plugin(ServerPlugin::new(()))
player_count: AtomicUsize::new(0), .add_system_to_stage(EventLoop, default_event_handler)
}, .add_system_set(PlayerList::default_system_set())
ServerState { .add_startup_system(setup)
player_list: None, .add_system(init_clients)
particle_list: create_particle_vec(), .add_system(despawn_disconnected_clients)
particle_idx: 0, .add_system(manage_particles)
}, .run();
)
} }
struct Game { #[derive(Resource)]
player_count: AtomicUsize, struct ParticleSpawner {
particles: Vec<Particle>,
index: usize,
} }
struct ServerState { impl ParticleSpawner {
player_list: Option<PlayerListId>, pub fn new() -> Self {
particle_list: Vec<Particle>, Self {
particle_idx: usize, particles: create_particle_vec(),
} index: 0,
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(),
),
} }
} }
fn init(&self, server: &mut Server<Self>) { pub fn next(&mut self) {
let (_, world) = server.worlds.insert(DimensionId::default(), ()); self.index = (self.index + 1) % self.particles.len();
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;
}
} }
} }
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:?}"); let string = format!("{dbg:?}");
string string
@ -253,7 +153,7 @@ fn create_particle_vec() -> Vec<Particle> {
Particle::Item(None), Particle::Item(None),
Particle::Item(Some(ItemStack::new(ItemKind::IronPickaxe, 1, None))), Particle::Item(Some(ItemStack::new(ItemKind::IronPickaxe, 1, None))),
Particle::VibrationBlock { Particle::VibrationBlock {
block_pos: SPAWN_POS, block_pos: [0, SPAWN_Y, 0].into(),
ticks: 50, ticks: 50,
}, },
Particle::VibrationEntity { Particle::VibrationEntity {

View 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());
}
}
}

View file

@ -1,207 +1,101 @@
use std::net::SocketAddr; use valence::client::despawn_disconnected_clients;
use std::sync::atomic::{AtomicUsize, Ordering}; use valence::client::event::{
default_event_handler, InteractWithEntity, ResourcePackStatus, ResourcePackStatusChange,
};
use valence::prelude::*; use valence::prelude::*;
use valence_protocol::types::EntityInteraction; use valence_protocol::types::EntityInteraction;
pub fn main() -> ShutdownResult { const SPAWN_Y: i32 = 64;
pub fn main() {
tracing_subscriber::fmt().init(); tracing_subscriber::fmt().init();
valence::start_server( App::new()
Game { .add_plugin(ServerPlugin::new(()))
player_count: AtomicUsize::new(0), .add_system_to_stage(EventLoop, default_event_handler)
}, .add_system_to_stage(EventLoop, prompt_on_punch)
ServerState { .add_system_to_stage(EventLoop, on_resource_pack_status)
player_list: None, .add_system_set(PlayerList::default_system_set())
sheep_id: EntityId::NULL, .add_startup_system(setup)
}, .add_system(init_clients)
) .add_system(despawn_disconnected_clients)
.run();
} }
struct Game { fn setup(world: &mut World) {
player_count: AtomicUsize, let mut instance = world
} .resource::<Server>()
.new_instance(DimensionId::default());
struct ServerState { for z in -5..5 {
player_list: Option<PlayerListId>, for x in -5..5 {
sheep_id: EntityId, instance.insert_chunk([x, z], Chunk::default());
}
#[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(),
),
} }
} }
fn init(&self, server: &mut Server<Self>) { for z in -25..25 {
let (world_id, world) = server.worlds.insert(DimensionId::default(), ()); for x in -25..25 {
server.state.player_list = Some(server.player_lists.insert(()).0); instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
let size = 5;
for z in -size..size {
for x in -size..size {
world.chunks.insert([x, z], UnloadedChunk::default(), ());
}
} }
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 instance_ent = world.spawn(instance).id();
let (world_id, _) = server.worlds.iter_mut().next().expect("missing world");
server.clients.retain(|_, client| { let mut sheep = McEntity::new(EntityKind::Sheep, instance_ent);
if client.created_this_tick() { sheep.set_position([0.0, SPAWN_Y as f64 + 1.0, 2.0]);
if self world.spawn(sheep);
.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 fn init_clients(
.entities mut clients: Query<&mut Client, Added<Client>>,
.insert_with_uuid(EntityKind::Player, client.uuid(), ()) instances: Query<Entity, With<Instance>>,
{ ) {
Some((id, _)) => client.entity_id = id, for mut client in &mut clients {
None => { client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.disconnect("Conflicting UUID"); client.set_instance(instances.single());
return false; client.set_game_mode(GameMode::Creative);
}
}
client.respawn(world_id); client.send_message("Hit the sheep to prompt for the resource pack.".italic());
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
});
} }
} }
/// Sends the resource pack prompt. fn prompt_on_punch(mut clients: Query<&mut Client>, mut events: EventReader<InteractWithEntity>) {
fn set_example_pack(client: &mut Client<Game>) { for event in events.iter() {
client.set_resource_pack( let Ok(mut client) = clients.get_mut(event.client) else {
"https://github.com/valence-rs/valence/raw/main/assets/example_pack.zip", continue;
"d7c6108849fb190ec2a49f2d38b7f1f897d9ce9f", };
false, if event.interact == EntityInteraction::Attack {
None, 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));
}
}
}
} }

View 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))
}
}

View file

@ -1,313 +1,317 @@
use std::net::SocketAddr; use std::collections::hash_map::Entry;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::SystemTime; use std::time::SystemTime;
use flume::{Receiver, Sender};
use noise::{NoiseFn, SuperSimplex}; use noise::{NoiseFn, SuperSimplex};
use rayon::iter::ParallelIterator; use tracing::info;
pub use valence::prelude::*; use valence::client::despawn_disconnected_clients;
use vek::Lerp; 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(); 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 seconds_per_day = 86_400;
let seed = (SystemTime::now() let seed = (SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)? .duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() .as_secs()
/ seconds_per_day) as u32; / seconds_per_day) as u32;
valence::start_server( info!("current seed: {seed}");
Game {
player_count: AtomicUsize::new(0), let (finished_sender, finished_receiver) = flume::unbounded();
density_noise: SuperSimplex::new(seed), let (pending_sender, pending_receiver) = flume::unbounded();
hilly_noise: SuperSimplex::new(seed.wrapping_add(1)),
stone_noise: SuperSimplex::new(seed.wrapping_add(2)), let state = Arc::new(ChunkWorkerState {
gravel_noise: SuperSimplex::new(seed.wrapping_add(3)), sender: finished_sender,
grass_noise: SuperSimplex::new(seed.wrapping_add(4)), receiver: pending_receiver,
}, density: SuperSimplex::new(seed),
None, 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 { fn init_clients(
player_count: AtomicUsize, mut clients: Query<&mut Client, Added<Client>>,
density_noise: SuperSimplex, instances: Query<Entity, With<Instance>>,
hilly_noise: SuperSimplex, mut commands: Commands,
stone_noise: SuperSimplex, ) {
gravel_noise: SuperSimplex, for mut client in &mut clients {
grass_noise: SuperSimplex, 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] fn update_client_views(
impl Config for Game { mut instances: Query<&mut Instance>,
type ServerState = Option<PlayerListId>; mut clients: Query<&mut Client>,
type ClientState = EntityId; mut state: ResMut<GameState>,
type EntityState = (); ) {
type WorldState = (); let instance = instances.single_mut();
/// If the chunk should stay loaded at the end of the tick.
type ChunkState = bool;
type PlayerListState = ();
type InventoryState = ();
async fn server_list_ping( for client in &mut clients {
&self, let view = client.view();
_server: &SharedServer<Self>, let queue_pos = |pos| {
_remote_addr: SocketAddr, if instance.chunk(pos).is_none() {
_protocol_version: i32, match state.pending.entry(pos) {
) -> ServerListPing { Entry::Occupied(mut oe) => {
ServerListPing::Respond { if let Some(priority) = oe.get_mut() {
online_players: self.player_count.load(Ordering::SeqCst) as i32, let dist = view.pos.distance_squared(pos);
max_players: MAX_PLAYERS as i32, *priority = (*priority).min(dist);
player_sample: Default::default(), }
description: "Hello Valence!".color(Color::AQUA), }
favicon_png: Some( Entry::Vacant(ve) => {
include_bytes!("../../../assets/logo-64x64.png") let dist = view.pos.distance_squared(pos);
.as_slice() ve.insert(Some(dist));
.into(), }
), }
}
};
// 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>) { // Sort chunks by ascending priority.
server.worlds.insert(DimensionId::default(), ()); to_send.sort_unstable_by_key(|(pri, _)| *pri);
server.state = Some(server.player_lists.insert(()).0);
// 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>) { fn chunk_worker(state: Arc<ChunkWorkerState>) {
let (world_id, world) = server.worlds.iter_mut().next().unwrap(); while let Ok(pos) = state.receiver.recv() {
let mut chunk = Chunk::new(SECTION_COUNT);
server.clients.retain(|_, client| { for offset_z in 0..16 {
if client.created_this_tick() { for offset_x in 0..16 {
if self let x = offset_x as i32 + pos.x * 16;
.player_count let z = offset_z as i32 + pos.z * 16;
.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 let mut in_terrain = false;
.entities let mut depth = 0;
.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;
}
}
client.respawn(world_id); // Fill in the terrain column.
client.set_flat(true); for y in (0..chunk.section_count() as i32 * 16).rev() {
client.set_game_mode(GameMode::Creative); const WATER_HEIGHT: i32 = 55;
client.teleport([0.0, 200.0, 0.0], 0.0, 0.0);
client.set_player_list(server.state.clone());
if let Some(id) = &server.state { let p = DVec3::new(x as f64, y as f64, z as f64);
server.player_lists[id].insert(
client.uuid(),
client.username(),
client.textures().cloned(),
client.game_mode(),
0,
None,
true,
);
}
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]; if in_terrain {
while let Some(event) = client.next_event() { if depth > 0 {
event.handle_default(client, player); depth -= 1;
} if y < gravel_height {
BlockState::GRAVEL
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);
} else { } 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( fn has_terrain_at(state: &ChunkWorkerState, p: DVec3) -> bool {
g: &Game, let hilly = lerp(0.1, 1.0, noise01(&state.hilly, p / 400.0)).powi(2);
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),
);
let lower = 15.0 + 100.0 * hilly; let lower = 15.0 + 100.0 * hilly;
let upper = lower + 100.0 * hilly; let upper = lower + 100.0 * hilly;
if y as f64 <= lower { if p.y <= lower {
return true; return true;
} else if y as f64 >= upper { } else if p.y >= upper {
return false; 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 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 { fn lerpstep(edge0: f64, edge1: f64, x: f64) -> f64 {
if x <= edge0 { if x <= edge0 {
0.0 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 freq = 1.0;
let mut amp = 1.0; let mut amp = 1.0;
let mut amp_sum = 0.0; let mut amp_sum = 0.0;
let mut sum = 0.0; let mut sum = 0.0;
for _ in 0..octaves { for _ in 0..octaves {
let n = noise01(noise, p.map(|a| a * freq)); let n = noise01(noise, p * freq);
sum += n * amp; sum += n * amp;
amp_sum += amp; amp_sum += amp;
@ -337,6 +341,6 @@ fn fbm(noise: &SuperSimplex, p: [f64; 3], octaves: u32, lacunarity: f64, persist
sum / amp_sum sum / amp_sum
} }
fn noise01(noise: &SuperSimplex, xyz: [f64; 3]) -> f64 { fn noise01(noise: &SuperSimplex, p: DVec3) -> f64 {
(noise.get(xyz) + 1.0) / 2.0 (noise.get(p.to_array()) + 1.0) / 2.0
} }

View file

@ -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::prelude::*;
use valence_protocol::translation_key;
pub fn main() -> ShutdownResult { const SPAWN_Y: i32 = 64;
pub fn main() {
tracing_subscriber::fmt().init(); 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)] fn setup(world: &mut World) {
struct Game {} let mut instance = world
.resource::<Server>()
.new_instance(DimensionId::default());
#[derive(Default)] for z in -5..5 {
struct ClientState { for x in -5..5 {
entity_id: EntityId, instance.insert_chunk([x, z], Chunk::default());
}
#[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(),
} }
} }
fn init(&self, server: &mut Server<Self>) { for z in -25..25 {
server.state = ServerState { for x in -25..25 {
world: create_world(server), instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
};
}
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(), ());
} }
} }
// Create platform world.spawn(instance);
let platform_block = BlockState::GLASS; }
for z in 0..PLATFORM_Z { fn init_clients(
for x in 0..PLATFORM_X { mut clients: Query<&mut Client, Added<Client>>,
world instances: Query<Entity, With<Instance>>,
.chunks ) {
.set_block_state([x, FLOOR_Y, z], platform_block); 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);
world_id
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(),
);
}
} }

View file

@ -10,18 +10,21 @@ use valence_protocol::ident::Ident;
/// Identifies a particular [`Biome`] on the server. /// Identifies a particular [`Biome`] on the server.
/// ///
/// The default biome ID refers to the first biome added in the server's /// The default biome ID refers to the first biome added in
/// [configuration](crate::config::Config). /// [`ServerPlugin::biomes`].
/// ///
/// To obtain biome IDs for other biomes, call /// To obtain biome IDs for other biomes, see [`ServerPlugin::biomes`].
/// [`biomes`](crate::server::SharedServer::biomes). ///
/// [`ServerPlugin::biomes`]: crate::config::ServerPlugin::biomes
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct BiomeId(pub(crate) u16); pub struct BiomeId(pub(crate) u16);
/// Contains the configuration for a biome. /// Contains the configuration for a biome.
/// ///
/// Biomes are registered once at startup through /// 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)] #[derive(Clone, Debug)]
pub struct Biome { pub struct Biome {
/// The unique name for this biome. The name can be /// The unique name for this biome. The name can be
@ -40,8 +43,7 @@ pub struct Biome {
pub additions_sound: Option<BiomeAdditionsSound>, pub additions_sound: Option<BiomeAdditionsSound>,
pub mood_sound: Option<BiomeMoodSound>, pub mood_sound: Option<BiomeMoodSound>,
pub particle: Option<BiomeParticle>, pub particle: Option<BiomeParticle>,
// TODO: The following fields should be added if they can affect the appearance of the biome to // TODO
// clients.
// * depth: f32 // * depth: f32
// * temperature: f32 // * temperature: f32
// * scale: f32 // * scale: f32

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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

View file

@ -1,76 +1,48 @@
//! Configuration for the server.
use std::borrow::Cow;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use bevy_app::{App, Plugin};
use serde::Serialize; use serde::Serialize;
use tokio::runtime::Handle as TokioHandle; use tokio::runtime::Handle;
use tracing::error;
use uuid::Uuid; use uuid::Uuid;
use valence_protocol::text::Text; use valence_protocol::{Text, Username};
use valence_protocol::username::Username;
use valence_protocol::MAX_PACKET_SIZE;
use crate::biome::Biome; use crate::biome::Biome;
use crate::dimension::Dimension; use crate::dimension::Dimension;
use crate::server::{NewClientData, Server, SharedServer}; use crate::server::{NewClientInfo, SharedServer};
use crate::{Ticks, STANDARD_TPS};
/// A trait for the configuration of a server. #[derive(Clone)]
/// #[non_exhaustive]
/// This trait uses the [async_trait] attribute macro. It is exported at the pub struct ServerPlugin<A> {
/// root of this crate. async_trait will be removed once async fns in traits pub callbacks: Arc<A>,
/// are stabilized. /// The [`Handle`] to the tokio runtime the server will use. If `None` is
/// /// provided, the server will create its own tokio runtime at startup.
/// [async_trait]: https://docs.rs/async-trait/latest/async_trait/ ///
#[async_trait] /// # Default Value
#[allow(unused_variables)] ///
pub trait Config: Sized + Send + Sync + 'static { /// `None`
/// Custom state to store with the [`Server`]. pub tokio_handle: Option<Handle>,
type ServerState: Send + Sync; /// The maximum number of simultaneous connections allowed to the server.
/// Custom state to store with every [`Client`](crate::client::Client). /// This includes all connections, not just those past the login stage.
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.
/// ///
/// You will want this value to be somewhere above the maximum number of /// 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 /// players, since status pings should still succeed even when the server is
/// full. /// full.
/// ///
/// # Default Implementation /// # Default Value
/// ///
/// Currently returns `1024`. This may change in a future version. /// `1024`. This may change in a future version.
fn max_connections(&self) -> usize { pub max_connections: usize,
1024 /// The socket address the server will be bound to.
}
/// Called once at startup to get 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. /// `0.0.0.0:25565`, which will listen on every available network interface.
fn address(&self) -> SocketAddr { pub address: SocketAddr,
SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 25565).into() /// The ticks per second of the server. This is the number of game updates
} /// that should occur in one second.
/// Called once at startup to get the tick rate, which 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 /// 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 /// 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, /// Note that the official Minecraft client only processes packets at 20hz,
/// so there is little benefit to a tick rate higher than 20. /// so there is little benefit to a tick rate higher than 20.
/// ///
/// # Default Implementation /// # Default Value
/// ///
/// Returns [`STANDARD_TPS`]. /// [`DEFAULT_TPS`]
fn tick_rate(&self) -> Ticks { pub tps: i64,
STANDARD_TPS /// 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.
/// 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 Implementation /// # Default Value
/// ///
/// Returns [`ConnectionMode::Online`]. /// [`ConnectionMode::Online`]
fn connection_mode(&self) -> ConnectionMode { pub connection_mode: ConnectionMode,
ConnectionMode::Online /// The compression threshold to use for compressing packets. For a
}
/// Obtains the compression threshold to use for compressing packets. For a
/// compression threshold of `Some(N)`, packets with encoded lengths >= `N` /// 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 /// # Default Implementation
/// ///
/// If the connection mode is [`ConnectionMode::Online`], `Some(256)` is /// A default placeholder response is returned.
/// returned. Otherwise, compression is disabled. async fn server_list_ping(
fn compression_threshold(&self) -> Option<u32> { &self,
match self.connection_mode() { shared: &SharedServer,
ConnectionMode::Online => Some(256), remote_addr: SocketAddr,
_ => None, 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 /// Called upon every client login to obtain the full URL to use for session
/// server requests. This is done to authenticate player accounts. This /// server requests. This is done to authenticate player accounts. This
/// method is not called unless [online mode] is enabled. /// 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>`. /// `https://sessionserver.mojang.com/session/minecraft/hasJoined?username=<username>&serverId=<auth-digest>&ip=<player-ip>`.
/// ///
/// [online mode]: crate::config::ConnectionMode::Online /// [online mode]: crate::config::ConnectionMode::Online
fn session_server( async fn session_server(
&self, &self,
server: &SharedServer<Self>, shared: &SharedServer,
username: Username<&str>, username: Username<&str>,
auth_digest: &str, auth_digest: &str,
player_ip: &IpAddr, player_ip: &IpAddr,
) -> String { ) -> 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}") format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={auth_digest}&ip={player_ip}")
} else { } else {
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={auth_digest}") 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. /// The default async callbacks.
#[derive(Clone, Debug)] impl AsyncCallbacks for () {}
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,
}
/// Describes how new connections to the server are handled. /// Describes how new connections to the server are handled.
#[derive(Clone, PartialEq)]
#[non_exhaustive] #[non_exhaustive]
#[derive(Clone, PartialEq, Default)]
pub enum ConnectionMode { pub enum ConnectionMode {
/// The "online mode" fetches all player data (username, UUID, and skin) /// The "online mode" fetches all player data (username, UUID, and skin)
/// from the [configured session server] and enables encryption. /// 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. /// behind a proxy.
/// ///
/// [configured session server]: Config::session_server /// [configured session server]: AsyncCallbacks::session_server
#[default] Online {
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. /// Disables client authentication with the configured session server.
/// Clients can join with any username and UUID they choose, potentially /// Clients can join with any username and UUID they choose, potentially
/// gaining privileges they would not otherwise have. Additionally, /// gaining privileges they would not otherwise have. Additionally,
@ -396,32 +387,50 @@ pub enum ConnectionMode {
Velocity { Velocity {
/// The secret key used to prevent connections from outside Velocity. /// The secret key used to prevent connections from outside Velocity.
/// The proxy and Valence must be configured to use the same secret key. /// 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. /// Minecraft's standard ticks per second (TPS).
#[cfg(test)] pub const DEFAULT_TPS: i64 = 20;
pub(crate) struct MockConfig<S = (), Cl = (), E = (), W = (), Ch = (), P = (), I = ()> {
_marker: std::marker::PhantomData<(S, Cl, E, W, Ch, P, I)>, /// 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)] /// Represents an individual entry in the player sample.
impl<S, Cl, E, W, Ch, P, I> Config for MockConfig<S, Cl, E, W, Ch, P, I> #[derive(Clone, Debug, Serialize)]
where pub struct PlayerSampleEntry {
S: Send + Sync + 'static, /// The name of the player.
Cl: Default + Send + Sync + 'static, ///
E: Send + Sync + 'static, /// This string can contain
W: Send + Sync + 'static, /// [legacy formatting codes](https://minecraft.fandom.com/wiki/Formatting_codes).
Ch: Send + Sync + 'static, pub name: String,
P: Send + Sync + 'static, /// The player UUID.
I: Send + Sync + 'static, pub id: Uuid,
{
type ServerState = S;
type ClientState = Cl;
type EntityState = E;
type WorldState = W;
type ChunkState = Ch;
type PlayerListState = P;
type InventoryState = I;
} }

View file

@ -9,11 +9,13 @@ use crate::LIBRARY_NAMESPACE;
/// Identifies a particular [`Dimension`] on the server. /// Identifies a particular [`Dimension`] on the server.
/// ///
/// The default dimension ID refers to the first dimension added in the server's /// The default dimension ID refers to the first dimension added in
/// [configuration](crate::config::Config). /// [`ServerPlugin::dimensions`].
/// ///
/// To obtain dimension IDs for other dimensions, call /// To obtain dimension IDs for other dimensions, look at
/// [`dimensions`](crate::server::SharedServer::dimensions). /// [`ServerPlugin::dimensions`].
///
/// [`ServerPlugin::dimensions`]: crate::server::SharedServer::dimensions
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct DimensionId(pub(crate) u16); pub struct DimensionId(pub(crate) u16);
@ -28,7 +30,9 @@ impl DimensionId {
} }
/// The default dimension ID corresponds to the first element in the `Vec` /// 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 { impl Default for DimensionId {
fn default() -> Self { fn default() -> Self {
Self(0) Self(0)
@ -37,16 +41,16 @@ impl Default for DimensionId {
/// Contains the configuration for a dimension type. /// 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 /// dimension determines certain properties of the world such as its height and
/// ambient lighting. /// ambient lighting.
/// ///
/// In Minecraft, "dimension" and "dimension type" are two distinct concepts. /// In Minecraft, "dimension" and "dimension type" are two distinct concepts.
/// For instance, the Overworld and Nether are dimensions, each with /// For instance, the Overworld and Nether are dimensions, each with
/// their own dimension type. A dimension in this library is analogous to a /// 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)] #[derive(Clone, Debug)]
pub struct Dimension { pub struct Dimension {
/// When false, compasses will spin randomly. /// When false, compasses will spin randomly.
@ -70,8 +74,7 @@ pub struct Dimension {
/// * `0 <= height <= 4064` /// * `0 <= height <= 4064`
/// * `min_y + height <= 2032` /// * `min_y + height <= 2032`
pub height: i32, pub height: i32,
// TODO: The following fields should be added if they can affect the // TODO: add other fields.
// appearance of the dimension to clients.
// * infiniburn // * infiniburn
// * monster_spawn_light_level // * monster_spawn_light_level
// * monster_spawn_block_light_level // * monster_spawn_block_light_level

File diff suppressed because it is too large Load diff

View file

@ -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 uuid::Uuid;
use valence_protocol::entity_meta::*; use valence_protocol::entity_meta::*;

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

View 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: &sect.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) = &sect.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);
}
}

View 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()
}
}

View file

@ -4,8 +4,7 @@ use std::io::Write;
use arrayvec::ArrayVec; use arrayvec::ArrayVec;
use valence_protocol::{Encode, VarInt}; use valence_protocol::{Encode, VarInt};
use crate::chunk::{compact_u64s_len, encode_compact_u64s}; use crate::math::bit_width;
use crate::util::bit_width;
/// `HALF_LEN` must be equal to `ceil(LEN / 2)`. /// `HALF_LEN` must be equal to `ceil(LEN / 2)`.
#[derive(Clone, Debug)] #[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 { pub fn get(&self, idx: usize) -> T {
self.check_oob(idx); debug_assert!(idx < LEN);
match self { match self {
Self::Single(elem) => *elem, 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 { pub fn set(&mut self, idx: usize, val: T) -> T {
self.check_oob(idx); debug_assert!(idx < LEN);
match self { match self {
Self::Single(old_val) => { 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. /// Encodes the paletted container in the format that Minecraft expects.
/// ///
/// - **`writer`**: The [`Write`] instance to write the paletted container /// - **`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)] #[cfg(test)]
mod tests { mod tests {
use rand::Rng; use rand::Rng;

File diff suppressed because it is too large Load diff

View file

@ -1,79 +1,17 @@
//! <img src="https://raw.githubusercontent.com/rj00a/valence/main/assets/logo-full.svg" width="400"> //! Valence is a Minecraft server framework written in Rust.
//!
//! ---
//!
//! 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
#![doc( #![doc(
html_logo_url = "https://raw.githubusercontent.com/valence-rs/valence/main/assets/logo.svg", 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" 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( #![deny(
rustdoc::broken_intra_doc_links, rustdoc::broken_intra_doc_links,
rustdoc::private_intra_doc_links, rustdoc::private_intra_doc_links,
rustdoc::missing_crate_level_docs, rustdoc::missing_crate_level_docs,
rustdoc::invalid_codeblock_attributes, rustdoc::invalid_codeblock_attributes,
rustdoc::invalid_rust_codeblocks, rustdoc::invalid_rust_codeblocks,
rustdoc::bare_urls rustdoc::bare_urls,
rustdoc::invalid_html_tags
)] )]
#![warn( #![warn(
trivial_casts, trivial_casts,
@ -88,89 +26,77 @@
clippy::comparison_chain clippy::comparison_chain
)] )]
/// Used on [`Config`](config::Config) to allow for async methods in traits. use bevy_ecs::prelude::*;
/// pub use {
/// For more information see the [async_trait] crate. anyhow, async_trait, bevy_app, bevy_ecs, uuid, valence_nbt as nbt, valence_protocol as protocol,
/// };
/// [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};
pub mod biome; pub mod biome;
pub mod chunk;
pub mod client; pub mod client;
pub mod config; pub mod config;
pub mod dimension; pub mod dimension;
pub mod entity; pub mod entity;
pub mod instance;
pub mod inventory; pub mod inventory;
pub mod math;
mod packet; mod packet;
pub mod player_list; pub mod player_list;
pub mod player_textures; pub mod player_textures;
pub mod server; pub mod server;
mod slab; #[cfg(any(test, doctest))]
mod slab_rc; mod unit_test;
mod slab_versioned; pub mod view;
pub mod util;
pub mod world;
/// Use `valence::prelude::*` to import the most commonly used items from the
/// library.
pub mod prelude { 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 biome::{Biome, BiomeId};
pub use chunk::{Chunk, ChunkPos, Chunks, LoadedChunk, UnloadedChunk}; pub use client::Client;
pub use client::{Client, ClientEvent, ClientId, Clients}; pub use config::{
pub use config::{Config, ConnectionMode, PlayerSampleEntry, ServerListPing}; AsyncCallbacks, ConnectionMode, PlayerSampleEntry, ServerListPing, ServerPlugin,
};
pub use dimension::{Dimension, DimensionId}; pub use dimension::{Dimension, DimensionId};
pub use entity::{Entities, Entity, EntityEvent, EntityId, EntityKind, TrackedData}; pub use entity::{
pub use inventory::{Inventories, Inventory, InventoryId}; EntityAnimation, EntityKind, EntityStatus, McEntity, McEntityManager, TrackedData,
pub use player_list::{PlayerList, PlayerListEntry, PlayerListId, PlayerLists}; };
pub use server::{NewClientData, Server, SharedServer, ShutdownResult}; pub use glam::DVec3;
pub use util::{from_yaw_and_pitch, to_yaw_and_pitch}; 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 uuid::Uuid;
pub use valence_nbt::Compound; pub use valence_nbt::Compound;
pub use valence_protocol::block::{PropName, PropValue}; pub use valence_protocol::{BlockKind, BlockPos};
pub use valence_protocol::entity_meta::Pose; pub use view::{ChunkPos, ChunkView};
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};
use super::*; use super::*;
pub use crate::{async_trait, nbt, vek, Ticks, STANDARD_TPS};
} }
/// The namespace for this library used internally for /// A [`Component`] for marking entities that should be despawned at the end of
/// [identifiers](valence_protocol::ident::Ident). /// 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"; const LIBRARY_NAMESPACE: &str = "valence";
/// The most recent version of the [Velocity] proxy which has been tested to /// Let's pretend that [`NULL_ENTITY`] was created by spawning an entity,
/// work with Valence. The elements of the tuple are (major, minor, patch) /// immediately despawning it, and then stealing its [`Entity`] ID. The user
/// version numbers. /// doesn't need to know about this.
/// const NULL_ENTITY: Entity = Entity::from_bits(u64::MAX);
/// 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;

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

View file

@ -1,29 +1,30 @@
use std::io::Write; 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 { pub(crate) trait WritePacket {
fn write_packet<P>(&mut self, packet: &P) -> anyhow::Result<()> fn write_packet<P>(&mut self, packet: &P)
where where
P: EncodePacket + ?Sized; 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 { 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 where
P: EncodePacket + ?Sized, P: EncodePacket + ?Sized,
{ {
(*self).write_packet(packet) (*self).write_packet(packet)
} }
fn write_bytes(&mut self, bytes: &[u8]) -> anyhow::Result<()> { fn write_packet_bytes(&mut self, bytes: &[u8]) {
(*self).write_bytes(bytes) (*self).write_packet_bytes(bytes)
} }
} }
pub struct PacketWriter<'a> { pub(crate) struct PacketWriter<'a> {
buf: &'a mut Vec<u8>, buf: &'a mut Vec<u8>,
threshold: Option<u32>, threshold: Option<u32>,
scratch: &'a mut Vec<u8>, scratch: &'a mut Vec<u8>,
@ -40,18 +41,39 @@ impl<'a> PacketWriter<'a> {
} }
impl WritePacket for PacketWriter<'_> { impl WritePacket for PacketWriter<'_> {
fn write_packet<P>(&mut self, pkt: &P) -> anyhow::Result<()> fn write_packet<P>(&mut self, pkt: &P)
where where
P: EncodePacket + ?Sized, 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) encode_packet_compressed(self.buf, pkt, threshold, self.scratch)
} else { } else {
encode_packet(self.buf, pkt) 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<()> { fn write_packet_bytes(&mut self, bytes: &[u8]) {
Ok(self.buf.write_all(bytes)?) 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

View file

@ -1,35 +1,27 @@
//! Player skins and capes. //! Player skins and capes.
use anyhow::Context;
use base64::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
use valence_protocol::types::Property;
/// Contains URLs to the skin and cape of a player. /// 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)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct SignedPlayerTextures { pub struct PlayerTextures {
payload: Box<str>, /// URL to the player's skin texture.
signature: Box<str>, pub skin: Url,
skin_url: Box<str>, /// URL to the player's cape texture. May be absent if the player does not
cape_url: Option<Box<str>>, /// have a cape.
pub cape: Option<Url>,
} }
impl SignedPlayerTextures { impl PlayerTextures {
/// Constructs the signed player textures from payload and signature pub fn from_properties(props: &[Property]) -> anyhow::Result<Self> {
/// components in base64. let textures = props
/// .iter()
/// Note that this does not validate that the signature is valid for the .find(|p| p.name == "textures")
/// given payload. .context("no textures in property list")?;
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())?;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct Textures { struct Textures {
@ -49,38 +41,13 @@ impl SignedPlayerTextures {
url: Url, 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 { Ok(Self {
payload, skin: textures.skin.url,
signature, cape: textures.cape.map(|t| t.url),
skin_url: String::from(textures.textures.skin.url).into(),
cape_url: textures.textures.cape.map(|t| String::from(t.url).into()),
}) })
} }
/// 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()
}
} }

View file

@ -1,126 +1,87 @@
//! The heart of the server.
use std::error::Error;
use std::iter::FusedIterator; use std::iter::FusedIterator;
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::ops::{Deref, DerefMut}; use std::ops::Deref;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant}; 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}; use flume::{Receiver, Sender};
pub(crate) use packet_manager::{PlayPacketReceiver, PlayPacketSender};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use rayon::iter::ParallelIterator;
use reqwest::Client as ReqwestClient;
use rsa::{PublicKeyParts, RsaPrivateKey}; 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::runtime::{Handle, Runtime};
use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tokio::sync::Semaphore;
use tracing::{error, info, info_span, instrument, trace, warn};
use uuid::Uuid; use uuid::Uuid;
use valence_nbt::{compound, Compound, List}; use valence_nbt::{compound, Compound, List};
use valence_protocol::packets::c2s::handshake::HandshakeOwned; use valence_protocol::types::Property;
use valence_protocol::packets::c2s::login::LoginStart; use valence_protocol::{ident, Username};
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 crate::biome::{validate_biomes, Biome, BiomeId}; use crate::biome::{validate_biomes, Biome, BiomeId};
use crate::chunk::entity_partition::update_entity_partition; use crate::client::event::{event_loop_run_criteria, register_client_events};
use crate::client::{Client, Clients}; use crate::client::{update_clients, Client};
use crate::config::{Config, ConnectionMode, ServerListPing}; use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin};
use crate::dimension::{validate_dimensions, Dimension, DimensionId}; use crate::dimension::{validate_dimensions, Dimension, DimensionId};
use crate::entity::Entities; use crate::entity::{
use crate::inventory::Inventories; check_entity_invariants, deinit_despawned_entities, init_entities, update_entities,
use crate::player_list::PlayerLists; McEntityManager,
use crate::player_textures::SignedPlayerTextures; };
use crate::server::packet_manager::InitialPacketManager; use crate::instance::{
use crate::world::Worlds; check_instance_invariants, update_instances_post_client, update_instances_pre_client, Instance,
use crate::Ticks; };
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 byte_channel;
mod login; mod connect;
mod packet_manager; pub(crate) mod connection;
/// Contains the entire state of a running Minecraft server, accessible from /// Contains global server state accessible as a [`Resource`].
/// within the [init] and [update] functions. #[derive(Resource)]
/// pub struct Server {
/// [init]: crate::config::Config::init /// Incremented on every tick.
/// [update]: crate::config::Config::update current_tick: i64,
pub struct Server<C: Config> { shared: SharedServer,
/// 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,
} }
impl<C: Config> Server<C> { impl Deref for Server {
/// Returns the number of ticks that have elapsed since the server began. type Target = SharedServer;
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;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.state &self.shared
} }
} }
impl<C: Config> DerefMut for Server<C> { impl Server {
fn deref_mut(&mut self) -> &mut Self::Target { /// Provides a reference to the [`SharedServer`].
&mut self.state 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 /// The subset of global server state which can be shared between threads.
/// is accessible outside the [update] loop.
/// ///
/// `SharedServer`s are internally refcounted and can /// `SharedServer`s are internally refcounted and are inexpensive to clone.
/// be shared between threads. #[derive(Clone)]
/// pub struct SharedServer(Arc<SharedServerInner>);
/// [update]: crate::config::Config::update
pub struct SharedServer<C: Config>(Arc<SharedServerInner<C>>);
impl<C: Config> Clone for SharedServer<C> { struct SharedServerInner {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
struct SharedServerInner<C: Config> {
cfg: C,
address: SocketAddr, address: SocketAddr,
tick_rate: Ticks, tps: i64,
connection_mode: ConnectionMode, connection_mode: ConnectionMode,
compression_threshold: Option<u32>, compression_threshold: Option<u32>,
max_connections: usize, max_connections: usize,
@ -131,58 +92,36 @@ struct SharedServerInner<C: Config> {
/// Holding a runtime handle is not enough to keep tokio working. We need /// Holding a runtime handle is not enough to keep tokio working. We need
/// to store the runtime here so we don't drop it. /// to store the runtime here so we don't drop it.
_tokio_runtime: Option<Runtime>, _tokio_runtime: Option<Runtime>,
dimensions: Vec<Dimension>, dimensions: Arc<[Dimension]>,
biomes: Vec<Biome>, biomes: Arc<[Biome]>,
/// Contains info about dimensions, biomes, and chats. /// Contains info about dimensions, biomes, and chats.
/// Sent to all clients when joining. /// Sent to all clients when joining.
registry_codec: Compound, registry_codec: Compound,
/// The instant the server was started. /// The instant the server was started.
start_instant: Instant, start_instant: Instant,
/// Sender for new clients past the login stage.
new_clients_send: Sender<Client>,
/// Receiver for new clients past the login stage. /// Receiver for new clients past the login stage.
new_clients_send: Sender<NewClientMessage>, new_clients_recv: Receiver<Client>,
new_clients_recv: Receiver<NewClientMessage>,
/// A semaphore used to limit the number of simultaneous connections to the /// A semaphore used to limit the number of simultaneous connections to the
/// server. Closing this semaphore stops new connections. /// server. Closing this semaphore stops new connections.
connection_sema: Arc<Semaphore>, connection_sema: Arc<Semaphore>,
/// The result that will be returned when the server is shut down. /// 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. /// The RSA keypair used for encryption with clients.
rsa_key: RsaPrivateKey, rsa_key: RsaPrivateKey,
/// The public part of `rsa_key` encoded in DER, which is an ASN.1 format. /// The public part of `rsa_key` encoded in DER, which is an ASN.1 format.
/// This is sent to clients during the authentication process. /// This is sent to clients during the authentication process.
public_key_der: Box<[u8]>, public_key_der: Box<[u8]>,
/// For session server requests. /// For session server requests.
http_client: ReqwestClient, http_client: reqwest::Client,
} }
/// Contains information about a new client joining the server. impl SharedServer {
#[non_exhaustive] /// Creates a new [`Instance`] component with the given dimension.
pub struct NewClientData { #[must_use]
/// The username of the new client. pub fn new_instance(&self, dimension: DimensionId) -> Instance {
pub username: Username<String>, Instance::new(dimension, self)
/// 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
} }
/// Gets the socket address this server is bound to. /// Gets the socket address this server is bound to.
@ -190,9 +129,9 @@ impl<C: Config> SharedServer<C> {
self.0.address self.0.address
} }
/// Gets the configured tick rate of this server. /// Gets the configured ticks per second of this server.
pub fn tick_rate(&self) -> Ticks { pub fn tps(&self) -> i64 {
self.0.tick_rate self.0.tps
} }
/// Gets the connection mode of the server. /// Gets the connection mode of the server.
@ -227,10 +166,6 @@ impl<C: Config> SharedServer<C> {
} }
/// Obtains a [`Dimension`] by using its corresponding [`DimensionId`]. /// 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 { pub fn dimension(&self, id: DimensionId) -> &Dimension {
self.0 self.0
.dimensions .dimensions
@ -249,10 +184,6 @@ impl<C: Config> SharedServer<C> {
} }
/// Obtains a [`Biome`] by using its corresponding [`BiomeId`]. /// 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 { pub fn biome(&self, id: BiomeId) -> &Biome {
self.0.biomes.get(id.0 as usize).expect("invalid biome ID") 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 /// 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 /// You may want to disconnect all players with a message prior to calling
/// this function. /// this function.
pub fn shutdown<E>(&self, res: Result<(), E>) pub fn shutdown<E>(&self, res: Result<(), E>)
where where
E: Into<Box<dyn Error + Send + Sync + 'static>>, E: Into<anyhow::Error>,
{ {
self.0.connection_sema.close(); self.0.connection_sema.close();
*self.0.shutdown_result.lock().unwrap() = Some(res.map_err(|e| e.into())); *self.0.shutdown_result.lock().unwrap() = Some(res.map_err(|e| e.into()));
} }
} }
/// Consumes the configuration and starts the server. /// Contains information about a new client joining the server.
/// #[non_exhaustive]
/// This function blocks the current thread and returns once the server has shut pub struct NewClientInfo {
/// down, a runtime error occurs, or the configuration is found to be invalid. /// The username of the new client.
pub fn start_server<C: Config>(config: C, data: C::ServerState) -> ShutdownResult { pub username: Username<String>,
let shared = setup_server(config) /// The UUID of the new client.
.context("failed to initialize server") pub uuid: Uuid,
.map_err(Box::<dyn Error + Send + Sync + 'static>::from)?; /// The remote address of the new client.
pub ip: IpAddr,
let _guard = shared.tokio_handle().enter(); /// The client's properties from the game profile. Typically contains a
/// `textures` property with the skin and cape of the player.
let mut server = Server { pub properties: Vec<Property>,
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)
} }
#[instrument(skip_all)] pub fn build_plugin(
fn setup_server<C: Config>(cfg: C) -> anyhow::Result<SharedServer<C>> { plugin: &ServerPlugin<impl AsyncCallbacks>,
let max_connections = cfg.max_connections(); app: &mut App,
let address = cfg.address(); ) -> anyhow::Result<()> {
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();
ensure!( ensure!(
incoming_packet_capacity > 0, plugin.tps > 0,
"serverbound packet capacity must be nonzero" "configured tick rate must be greater than zero"
); );
let outgoing_packet_capacity = cfg.outgoing_capacity();
ensure!( ensure!(
outgoing_packet_capacity > 0, plugin.incoming_capacity > 0,
"outgoing packet capacity must be nonzero" "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)?; 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()) rsa_der::public_key_to_der(&rsa_key.n().to_bytes_be(), &rsa_key.e().to_bytes_be())
.into_boxed_slice(); .into_boxed_slice();
let (new_clients_send, new_clients_recv) = flume::bounded(64); let runtime = if plugin.tokio_handle.is_none() {
let runtime = if tokio_handle.is_none() {
Some(Runtime::new()?) Some(Runtime::new()?)
} else { } else {
None None
@ -373,56 +269,200 @@ fn setup_server<C: Config>(cfg: C) -> anyhow::Result<SharedServer<C>> {
let tokio_handle = match &runtime { let tokio_handle = match &runtime {
Some(rt) => rt.handle().clone(), 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 { let registry_codec = make_registry_codec(&plugin.dimensions, &plugin.biomes);
cfg,
address, let (new_clients_send, new_clients_recv) = flume::bounded(64);
tick_rate,
connection_mode, let shared = SharedServer(Arc::new(SharedServerInner {
compression_threshold, address: plugin.address,
max_connections, tps: plugin.tps,
incoming_capacity: incoming_packet_capacity, connection_mode: plugin.connection_mode.clone(),
outgoing_capacity: outgoing_packet_capacity, compression_threshold: plugin.compression_threshold,
max_connections: plugin.max_connections,
incoming_capacity: plugin.incoming_capacity,
outgoing_capacity: plugin.outgoing_capacity,
tokio_handle, tokio_handle,
_tokio_runtime: runtime, _tokio_runtime: runtime,
dimensions, dimensions: plugin.dimensions.clone(),
biomes, biomes: plugin.biomes.clone(),
registry_codec, registry_codec,
start_instant: Instant::now(), start_instant: Instant::now(),
new_clients_send, new_clients_send,
new_clients_recv, 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), shutdown_result: Mutex::new(None),
rsa_key, rsa_key,
public_key_der, 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 { fn make_registry_codec(dimensions: &[Dimension], biomes: &[Biome]) -> Compound {
compound! { let dimensions = dimensions
ident!("dimension_type") => compound! { .iter()
"type" => ident!("dimension_type"), .enumerate()
"value" => List::Compound(dimensions.iter().enumerate().map(|(id, dim)| compound! { .map(|(id, dim)| {
compound! {
"name" => DimensionId(id as u16).dimension_type_name(), "name" => DimensionId(id as u16).dimension_type_name(),
"id" => id as i32, "id" => id as i32,
"element" => dim.to_dimension_registry_item(), "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! { ident!("worldgen/biome") => compound! {
"type" => ident!("worldgen/biome"), "type" => ident!("worldgen/biome"),
"value" => { "value" => {
List::Compound(biomes List::Compound(biomes)
.iter()
.enumerate()
.map(|(id, biome)| biome.to_biome_registry_item(id as i32))
.collect())
} }
}, },
ident!("chat_type") => compound! { 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))
}

View file

@ -1,3 +1,5 @@
//! A channel specifically for sending/receiving batches of bytes.
#![allow(dead_code)] #![allow(dead_code)]
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -116,6 +118,10 @@ impl ByteSender {
pub fn is_disconnected(&self) -> bool { pub fn is_disconnected(&self) -> bool {
self.shared.mtx.lock().unwrap().disconnected self.shared.mtx.lock().unwrap().disconnected
} }
pub fn limit(&self) -> usize {
self.shared.limit
}
} }
/// Contains any excess bytes not sent. /// Contains any excess bytes not sent.
@ -175,6 +181,10 @@ impl ByteReceiver {
pub fn is_disconnected(&self) -> bool { pub fn is_disconnected(&self) -> bool {
self.shared.mtx.lock().unwrap().disconnected self.shared.mtx.lock().unwrap().disconnected
} }
pub fn limit(&self) -> usize {
self.shared.limit
}
} }
#[derive(Copy, Clone, PartialEq, Eq, Debug, Error)] #[derive(Copy, Clone, PartialEq, Eq, Debug, Error)]

View 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"
);
}
}

View file

@ -1,21 +1,23 @@
use std::fmt; use std::io;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::time::Duration; use std::time::Duration;
use anyhow::Result; use anyhow::bail;
use tokio::io; use bytes::BytesMut;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::runtime::Handle;
use tokio::sync::OwnedSemaphorePermit; use tokio::sync::OwnedSemaphorePermit;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tokio::time::timeout; use tokio::time::timeout;
use tracing::debug; use tracing::debug;
use valence_protocol::{DecodePacket, EncodePacket, PacketDecoder, PacketEncoder}; use valence_protocol::{DecodePacket, EncodePacket, PacketDecoder, PacketEncoder};
use crate::packet::WritePacket; use crate::client::{Client, ClientConnection};
use crate::server::byte_channel::{byte_channel, ByteReceiver, ByteSender, TryRecvError}; 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, reader: R,
writer: W, writer: W,
enc: PacketEncoder, enc: PacketEncoder,
@ -26,7 +28,7 @@ pub struct InitialPacketManager<R, W> {
const READ_BUF_SIZE: usize = 4096; const READ_BUF_SIZE: usize = 4096;
impl<R, W> InitialPacketManager<R, W> impl<R, W> InitialConnection<R, W>
where where
R: AsyncRead + Unpin, R: AsyncRead + Unpin,
W: AsyncWrite + 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 where
P: EncodePacket + ?Sized, P: EncodePacket + ?Sized,
{ {
@ -59,9 +61,9 @@ where
Ok(()) 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 where
P: DecodePacket<'a> + fmt::Debug, P: DecodePacket<'a>,
{ {
timeout(self.timeout, async { timeout(self.timeout, async {
while !self.dec.has_next_packet()? { while !self.dec.has_next_packet()? {
@ -117,12 +119,12 @@ where
self.dec.enable_encryption(key); self.dec.enable_encryption(key);
} }
pub fn into_play( pub fn into_client(
mut self, mut self,
info: NewClientInfo,
incoming_limit: usize, incoming_limit: usize,
outgoing_limit: usize, outgoing_limit: usize,
handle: Handle, ) -> Client
) -> (PlayPacketSender, PlayPacketReceiver, OwnedSemaphorePermit)
where where
R: Send + 'static, R: Send + 'static,
W: Send + 'static, W: Send + 'static,
@ -168,118 +170,55 @@ where
} }
}); });
( Client::new(
PlayPacketSender { info,
enc: self.enc, Box::new(RealClientConnection {
send: outgoing_sender, send: outgoing_sender,
writer_task: Some(writer_task),
handle,
},
PlayPacketReceiver {
dec: self.dec,
recv: incoming_receiver, recv: incoming_receiver,
_permit: self.permit,
reader_task, reader_task,
}, writer_task,
self.permit, }),
self.enc,
self.dec,
) )
} }
} }
/// Manages a packet encoder and a byte channel to send the encoded packets struct RealClientConnection {
/// through.
pub struct PlayPacketSender {
enc: PacketEncoder,
send: ByteSender, 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, recv: ByteReceiver,
/// Ensures that we don't allow more connections to the server until the
/// client is dropped.
_permit: OwnedSemaphorePermit,
reader_task: JoinHandle<()>, reader_task: JoinHandle<()>,
writer_task: JoinHandle<()>,
} }
impl PlayPacketReceiver { impl Drop for RealClientConnection {
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 {
fn drop(&mut self) { fn drop(&mut self) {
self.writer_task.abort();
self.reader_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"),
}
}
}

View file

@ -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"
);
}
}

View file

@ -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)
}
}

View file

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

View file

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

View 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(())
}
}

View file

@ -0,0 +1,2 @@
mod example;
pub(crate) mod util;

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

View file

@ -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
View 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);
}
}

View file

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

View file

@ -2,7 +2,7 @@
name = "valence_anvil" name = "valence_anvil"
description = "A library for Minecraft's Anvil world format." description = "A library for Minecraft's Anvil world format."
documentation = "https://docs.rs/valence_anvil/" 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" readme = "README.md"
license = "MIT" license = "MIT"
keywords = ["anvil", "minecraft", "deserialization"] keywords = ["anvil", "minecraft", "deserialization"]
@ -20,11 +20,14 @@ valence_nbt = { version = "0.5.0", path = "../valence_nbt" }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.68" anyhow = "1.0.68"
bevy_ecs = "0.9.1"
clap = "4.1.4"
criterion = "0.4.0" criterion = "0.4.0"
flume = "0.10.14"
fs_extra = "1.2.0" fs_extra = "1.2.0"
tempfile = "3.3.0" tempfile = "3.3.0"
valence = { version = "0.2.0", path = "../valence" } tracing = "0.1.37"
valence_anvil = { version = "0.1.0", path = ".", features = ["valence"] } tracing-subscriber = "0.3.16"
zip = "0.6.3" zip = "0.6.3"
[dev-dependencies.reqwest] [dev-dependencies.reqwest]
@ -36,3 +39,6 @@ features = ["rustls-tls", "blocking", "stream"]
[[bench]] [[bench]]
name = "world_parsing" name = "world_parsing"
harness = false harness = false
[features]
default = ["valence"]

View file

@ -5,7 +5,7 @@ use anyhow::{ensure, Context};
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
use fs_extra::dir::CopyOptions; use fs_extra::dir::CopyOptions;
use reqwest::IntoUrl; use reqwest::IntoUrl;
use valence::chunk::UnloadedChunk; use valence::instance::Chunk;
use valence_anvil::AnvilWorld; use valence_anvil::AnvilWorld;
use zip::ZipArchive; use zip::ZipArchive;
@ -33,7 +33,7 @@ fn criterion_benchmark(c: &mut Criterion) {
.expect("missing chunk at position") .expect("missing chunk at position")
.data; .data;
let mut chunk = UnloadedChunk::new(24); let mut chunk = Chunk::new(24);
valence_anvil::to_valence(&nbt, &mut chunk, 4, |_| Default::default()).unwrap(); valence_anvil::to_valence(&nbt, &mut chunk, 4, |_| Default::default()).unwrap();

View 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))
}

View file

@ -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)
}
}
}
}

View file

@ -1,7 +1,7 @@
use num_integer::div_ceil; use num_integer::div_ceil;
use thiserror::Error; use thiserror::Error;
use valence::biome::BiomeId; use valence::biome::BiomeId;
use valence::chunk::Chunk; use valence::instance::Chunk;
use valence::protocol::block::{BlockKind, PropName, PropValue}; use valence::protocol::block::{BlockKind, PropName, PropValue};
use valence::protocol::Ident; use valence::protocol::Ident;
use valence_nbt::{Compound, List, Value}; use valence_nbt::{Compound, List, Value};
@ -53,14 +53,14 @@ pub enum ToValenceError {
BadBiomePaletteIndex, 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 /// An error is returned if the NBT data does not match the expected structure
/// for an Anvil chunk. /// for an Anvil chunk.
/// ///
/// # Arguments /// # Arguments
/// ///
/// - `nbt`: The Anvil chunk to read from. This is usually the value returned by /// - `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. /// - `chunk`: The Valence chunk to write to.
/// - `sect_offset`: A constant to add to all sector Y positions in `nbt`. After /// - `sect_offset`: A constant to add to all sector Y positions in `nbt`. After
/// applying the offset, only the sectors in the range /// 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 /// - `map_biome`: A function to map biome resource identifiers in the NBT data
/// to Valence [`BiomeId`]s. /// to Valence [`BiomeId`]s.
/// ///
/// [`read_chunk`]: crate::AnvilWorld::read_chunk /// [`AnvilWorld::read_chunk`]: crate::AnvilWorld::read_chunk
pub fn to_valence<C, F>( pub fn to_valence<F, const LOADED: bool>(
nbt: &Compound, nbt: &Compound,
chunk: &mut C, chunk: &mut Chunk<LOADED>,
sect_offset: i32, sect_offset: i32,
mut map_biome: F, mut map_biome: F,
) -> Result<(), ToValenceError> ) -> Result<(), ToValenceError>
where where
C: Chunk,
F: FnMut(Ident<&str>) -> BiomeId, F: FnMut(Ident<&str>) -> BiomeId,
{ {
let Some(Value::List(List::Compound(sections))) = nbt.get("sections") else { let Some(Value::List(List::Compound(sections))) = nbt.get("sections") else {

View file

@ -17,7 +17,7 @@ serde_json = "1.0.87"
thiserror = "1.0.37" thiserror = "1.0.37"
tracing = "0.1.37" tracing = "0.1.37"
uuid = { version = "1.2.1", features = ["serde"] } 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" } valence_nbt = { version = "0.5.0", path = "../valence_nbt" }
[[bench]] [[bench]]

View file

@ -130,12 +130,13 @@ fn packets(c: &mut Criterion) {
}; };
let tab_list_header_footer_packet = SetTabListHeaderAndFooter { let tab_list_header_footer_packet = SetTabListHeaderAndFooter {
header: "this".italic() + " is the " + "header".bold().color(Color::RED), header: ("this".italic() + " is the " + "header".bold().color(Color::RED)).into(),
footer: "this".italic() footer: ("this".italic()
+ " is the " + " is the "
+ "footer".bold().color(Color::BLUE) + "footer".bold().color(Color::BLUE)
+ ". I am appending some extra text so that the packet goes over the compression \ + ". I am appending some extra text so that the packet goes over the compression \
threshold.", threshold.")
.into(),
}; };
let spawn_entity_packet = SpawnEntity { let spawn_entity_packet = SpawnEntity {

View file

@ -1,5 +1,3 @@
use std::fmt;
#[cfg(feature = "encryption")] #[cfg(feature = "encryption")]
use aes::cipher::{AsyncStreamCipher, NewCipher}; use aes::cipher::{AsyncStreamCipher, NewCipher};
use anyhow::{bail, ensure}; use anyhow::{bail, ensure};
@ -30,6 +28,7 @@ impl PacketEncoder {
Self::default() Self::default()
} }
#[inline]
pub fn append_bytes(&mut self, bytes: &[u8]) { pub fn append_bytes(&mut self, bytes: &[u8]) {
self.buf.extend_from_slice(bytes) 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>> pub fn try_next_packet<'a, P>(&'a mut self) -> Result<Option<P>>
where where
P: DecodePacket<'a> + fmt::Debug, P: DecodePacket<'a>,
{ {
self.buf.advance(self.cursor); self.buf.advance(self.cursor);
self.cursor = 0; self.cursor = 0;
@ -362,6 +361,69 @@ impl PacketDecoder {
Ok(Some(packet)) 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> { pub fn has_next_packet(&self) -> Result<bool> {
let mut r = &self.buf[self.cursor..]; let mut r = &self.buf[self.cursor..];
@ -535,4 +597,25 @@ mod tests {
.unwrap() .unwrap()
.check("third"); .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);
}
} }

View file

@ -22,6 +22,7 @@ impl Encode for bool {
} }
fn write_slice(slice: &[bool], mut w: impl Write) -> io::Result<()> { 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) }; let bytes: &[u8] = unsafe { mem::transmute(slice) };
w.write_all(bytes) w.write_all(bytes)
} }
@ -32,7 +33,7 @@ impl Encode for bool {
impl Decode<'_> for bool { impl Decode<'_> for bool {
fn decode(r: &mut &[u8]) -> Result<Self> { fn decode(r: &mut &[u8]) -> Result<Self> {
let n = r.read_u8()?; 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) 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> impl<'a, B> Encode for Cow<'a, B>
where where
B: ToOwned + Encode, B: ToOwned + Encode + ?Sized,
{ {
fn encode(&self, w: impl Write) -> Result<()> { fn encode(&self, w: impl Write) -> Result<()> {
self.as_ref().encode(w) 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 where
B: ToOwned, B: ToOwned + ?Sized,
&'a B: Decode<'a>, B::Owned: Decode<'a>,
{ {
fn decode(r: &mut &'a [u8]) -> Result<Self> { fn decode(r: &mut &'a [u8]) -> Result<Self> {
<&B>::decode(r).map(Cow::Borrowed) B::Owned::decode(r).map(Cow::Owned)
} }
} }

View file

@ -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,
}
}
}

View file

@ -68,8 +68,8 @@
// Allows us to use our own proc macros internally. // Allows us to use our own proc macros internally.
extern crate self as valence_protocol; extern crate self as valence_protocol;
use std::io;
use std::io::Write; use std::io::Write;
use std::{fmt, io};
pub use anyhow::{Error, Result}; pub use anyhow::{Error, Result};
pub use array::LengthPrefixedArray; pub use array::LengthPrefixedArray;
@ -78,13 +78,12 @@ pub use block_pos::BlockPos;
pub use byte_angle::ByteAngle; pub use byte_angle::ByteAngle;
pub use codec::*; pub use codec::*;
pub use ident::Ident; pub use ident::Ident;
pub use inventory::InventoryKind;
pub use item::{ItemKind, ItemStack}; pub use item::{ItemKind, ItemStack};
pub use raw_bytes::RawBytes; pub use raw_bytes::RawBytes;
pub use text::{Text, TextFormat}; pub use text::{Text, TextFormat};
pub use username::Username; pub use username::Username;
pub use uuid::Uuid; 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_int::VarInt;
pub use var_long::VarLong; pub use var_long::VarLong;
pub use {uuid, valence_nbt as nbt}; pub use {uuid, valence_nbt as nbt};
@ -106,7 +105,6 @@ pub mod enchant;
pub mod entity_meta; pub mod entity_meta;
pub mod ident; pub mod ident;
mod impls; mod impls;
mod inventory;
mod item; mod item;
pub mod packets; pub mod packets;
mod raw_bytes; mod raw_bytes;
@ -174,7 +172,7 @@ pub const MAX_PACKET_SIZE: i32 = 2097152;
/// println!("{buf:?}"); /// println!("{buf:?}");
/// ``` /// ```
/// ///
/// [macro]: valence_derive::Encode /// [macro]: valence_protocol_macros::Encode
pub trait Encode { pub trait Encode {
/// Writes this object to the provided writer. /// Writes this object to the provided writer.
/// ///
@ -249,7 +247,7 @@ pub trait Encode {
/// assert!(r.is_empty()); /// assert!(r.is_empty());
/// ``` /// ```
/// ///
/// [macro]: valence_derive::Decode /// [macro]: valence_protocol_macros::Decode
pub trait Decode<'a>: Sized { pub trait Decode<'a>: Sized {
/// Reads this object from the provided byte slice. /// Reads this object from the provided byte slice.
/// ///
@ -284,8 +282,8 @@ pub trait Decode<'a>: Sized {
/// println!("{buf:?}"); /// println!("{buf:?}");
/// ``` /// ```
/// ///
/// [macro]: valence_derive::DecodePacket /// [macro]: valence_protocol_macros::DecodePacket
pub trait EncodePacket { pub trait EncodePacket: fmt::Debug {
/// The packet ID that is written when [`Self::encode_packet`] is called. A /// The packet ID that is written when [`Self::encode_packet`] is called. A
/// negative value indicates that the packet ID is not statically known. /// negative value indicates that the packet ID is not statically known.
const PACKET_ID: i32 = -1; const PACKET_ID: i32 = -1;
@ -324,7 +322,7 @@ pub trait EncodePacket {
/// ``` /// ```
/// ///
/// [macro]: valence_protocol::DecodePacket /// [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 /// The packet ID that is read when [`Self::decode_packet`] is called. A
/// negative value indicates that the packet ID is not statically known. /// negative value indicates that the packet ID is not statically known.
const PACKET_ID: i32 = -1; const PACKET_ID: i32 = -1;
@ -339,7 +337,7 @@ pub trait DecodePacket<'a>: Sized {
mod derive_tests { mod derive_tests {
use super::*; use super::*;
#[derive(Encode, EncodePacket, Decode, DecodePacket)] #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)]
#[packet_id = 1] #[packet_id = 1]
struct RegularStruct { struct RegularStruct {
foo: i32, foo: i32,
@ -347,30 +345,30 @@ mod derive_tests {
baz: f64, baz: f64,
} }
#[derive(Encode, EncodePacket, Decode, DecodePacket)] #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)]
#[packet_id = 2] #[packet_id = 2]
struct UnitStruct; struct UnitStruct;
#[derive(Encode, EncodePacket, Decode, DecodePacket)] #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)]
#[packet_id = 3] #[packet_id = 3]
struct EmptyStruct {} struct EmptyStruct {}
#[derive(Encode, EncodePacket, Decode, DecodePacket)] #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)]
#[packet_id = 4] #[packet_id = 4]
struct TupleStruct(i32, bool, f64); struct TupleStruct(i32, bool, f64);
#[derive(Encode, EncodePacket, Decode, DecodePacket)] #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)]
#[packet_id = 5] #[packet_id = 5]
struct StructWithGenerics<'z, T = ()> { struct StructWithGenerics<'z, T: std::fmt::Debug = ()> {
foo: &'z str, foo: &'z str,
bar: T, bar: T,
} }
#[derive(Encode, EncodePacket, Decode, DecodePacket)] #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)]
#[packet_id = 6] #[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] #[packet_id = 7]
enum RegularEnum { enum RegularEnum {
Empty, Empty,
@ -378,13 +376,13 @@ mod derive_tests {
Fields { foo: i32, bar: bool, baz: f64 }, Fields { foo: i32, bar: bool, baz: f64 },
} }
#[derive(Encode, EncodePacket, Decode, DecodePacket)] #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)]
#[packet_id = 8] #[packet_id = 8]
enum EmptyEnum {} enum EmptyEnum {}
#[derive(Encode, EncodePacket, Decode, DecodePacket)] #[derive(Encode, EncodePacket, Decode, DecodePacket, Debug)]
#[packet_id = 0xbeef] #[packet_id = 0xbeef]
enum EnumWithGenericsAndTags<'z, T = ()> { enum EnumWithGenericsAndTags<'z, T: std::fmt::Debug = ()> {
#[tag = 5] #[tag = 5]
First { First {
foo: &'z str, foo: &'z str,

View file

@ -1,5 +1,6 @@
use std::borrow::Cow;
use uuid::Uuid; use uuid::Uuid;
use valence_derive::{Decode, DecodePacket, Encode, EncodePacket};
use valence_nbt::Compound; use valence_nbt::Compound;
use crate::block_pos::BlockPos; use crate::block_pos::BlockPos;
@ -11,14 +12,14 @@ use crate::text::Text;
use crate::types::{ use crate::types::{
AttributeProperty, BossBarAction, ChatSuggestionAction, ChunkDataBlockEntity, AttributeProperty, BossBarAction, ChatSuggestionAction, ChunkDataBlockEntity,
CommandSuggestionMatch, Difficulty, EntityEffectFlags, FeetOrEyes, GameEventKind, GameMode, CommandSuggestionMatch, Difficulty, EntityEffectFlags, FeetOrEyes, GameEventKind, GameMode,
GlobalPos, Hand, LookAtEntity, MerchantTrade, PlayerAbilitiesFlags, SignedProperty, GlobalPos, Hand, LookAtEntity, MerchantTrade, PlayerAbilitiesFlags, Property, SoundCategory,
SoundCategory, Statistic, SyncPlayerPosLookFlags, TagGroup, UpdateObjectiveMode, Statistic, SyncPlayerPosLookFlags, TagGroup, UpdateObjectiveMode, UpdateScoreAction,
UpdateScoreAction, WindowType,
}; };
use crate::username::Username; use crate::username::Username;
use crate::var_int::VarInt; use crate::var_int::VarInt;
use crate::var_long::VarLong; use crate::var_long::VarLong;
use crate::LengthPrefixedArray; use crate::{Decode, DecodePacket, Encode, EncodePacket, LengthPrefixedArray};
pub mod commands; pub mod commands;
pub mod declare_recipes; pub mod declare_recipes;
@ -63,8 +64,8 @@ pub mod login {
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x00] #[packet_id = 0x00]
pub struct DisconnectLogin { pub struct DisconnectLogin<'a> {
pub reason: Text, pub reason: Cow<'a, Text>,
} }
#[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
@ -80,7 +81,7 @@ pub mod login {
pub struct LoginSuccess<'a> { pub struct LoginSuccess<'a> {
pub uuid: Uuid, pub uuid: Uuid,
pub username: Username<&'a str>, pub username: Username<&'a str>,
pub properties: Vec<SignedProperty<'a>>, pub properties: Cow<'a, [Property]>,
} }
#[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
@ -100,7 +101,7 @@ pub mod login {
packet_enum! { packet_enum! {
#[derive(Clone)] #[derive(Clone)]
S2cLoginPacket<'a> { S2cLoginPacket<'a> {
DisconnectLogin, DisconnectLogin<'a>,
EncryptionRequest<'a>, EncryptionRequest<'a>,
LoginSuccess<'a>, LoginSuccess<'a>,
SetCompression, SetCompression,
@ -327,17 +328,17 @@ pub mod play {
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x17] #[packet_id = 0x17]
pub struct DisconnectPlay { pub struct DisconnectPlay<'a> {
pub reason: Text, pub reason: Cow<'a, Text>,
} }
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x18] #[packet_id = 0x18]
pub struct DisguisedChatMessage { pub struct DisguisedChatMessage<'a> {
pub message: Text, pub message: Cow<'a, Text>,
pub chat_type: VarInt, pub chat_type: VarInt,
pub chat_type_name: Text, pub chat_type_name: Cow<'a, Text>,
pub target_name: Option<Text>, pub target_name: Option<Cow<'a, Text>>,
} }
#[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
@ -553,10 +554,10 @@ pub mod play {
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x2c] #[packet_id = 0x2c]
pub struct OpenScreen { pub struct OpenScreen<'a> {
pub window_id: VarInt, pub window_id: VarInt,
pub window_type: VarInt, pub window_type: WindowType,
pub window_title: Text, pub window_title: Cow<'a, Text>,
} }
#[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
@ -601,17 +602,17 @@ pub mod play {
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x34] #[packet_id = 0x34]
pub struct CombatDeath { pub struct CombatDeath<'a> {
pub player_id: VarInt, pub player_id: VarInt,
/// Killer's entity ID, -1 if no killer /// Killer's entity ID, -1 if no killer
pub entity_id: i32, pub entity_id: i32,
pub message: Text, pub message: Cow<'a, Text>,
} }
#[derive(Clone, PartialEq, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, PartialEq, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x35] #[packet_id = 0x35]
pub struct PlayerInfoRemove { pub struct PlayerInfoRemove<'a> {
pub players: Vec<Uuid>, pub uuids: Cow<'a, [Uuid]>,
} }
#[derive(Copy, Clone, PartialEq, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Copy, Clone, PartialEq, Debug, Encode, EncodePacket, Decode, DecodePacket)]
@ -658,7 +659,7 @@ pub mod play {
pub url: &'a str, pub url: &'a str,
pub hash: &'a str, pub hash: &'a str,
pub forced: bool, pub forced: bool,
pub prompt_message: Option<Text>, pub prompt_message: Option<Cow<'a, Text>>,
} }
#[derive(Clone, PartialEq, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, PartialEq, Debug, Encode, EncodePacket, Decode, DecodePacket)]
@ -722,15 +723,15 @@ pub mod play {
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x41] #[packet_id = 0x41]
pub struct ServerData<'a> { pub struct ServerData<'a> {
pub motd: Option<Text>, pub motd: Option<Cow<'a, Text>>,
pub icon: Option<&'a str>, pub icon: Option<&'a str>,
pub enforce_secure_chat: bool, pub enforce_secure_chat: bool,
} }
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x42] #[packet_id = 0x42]
pub struct SetActionBarText { pub struct SetActionBarText<'a> {
pub action_bar_text: Text, pub action_bar_text: Cow<'a, Text>,
} }
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
@ -878,8 +879,8 @@ pub mod play {
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x59] #[packet_id = 0x59]
pub struct SetSubtitleText { pub struct SetSubtitleText<'a> {
pub subtitle_text: Text, pub subtitle_text: Cow<'a, Text>,
} }
#[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
@ -895,8 +896,8 @@ pub mod play {
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x5b] #[packet_id = 0x5b]
pub struct SetTitleText { pub struct SetTitleText<'a> {
pub title_text: Text, pub title_text: Cow<'a, Text>,
} }
#[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
@ -934,17 +935,17 @@ pub mod play {
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x60] #[packet_id = 0x60]
pub struct SystemChatMessage { pub struct SystemChatMessage<'a> {
pub chat: Text, pub chat: Cow<'a, Text>,
/// Whether the message is in the actionbar or the chat. /// Whether the message is in the actionbar or the chat.
pub overlay: bool, pub overlay: bool,
} }
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
#[packet_id = 0x61] #[packet_id = 0x61]
pub struct SetTabListHeaderAndFooter { pub struct SetTabListHeaderAndFooter<'a> {
pub header: Text, pub header: Cow<'a, Text>,
pub footer: Text, pub footer: Cow<'a, Text>,
} }
#[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)]
@ -1034,8 +1035,8 @@ pub mod play {
ChatSuggestions<'a>, ChatSuggestions<'a>,
PluginMessageS2c<'a>, PluginMessageS2c<'a>,
DeleteMessage<'a>, DeleteMessage<'a>,
DisconnectPlay, DisconnectPlay<'a>,
DisguisedChatMessage, DisguisedChatMessage<'a>,
EntityEvent, EntityEvent,
PlaceRecipe<'a>, PlaceRecipe<'a>,
UnloadChunk, UnloadChunk,
@ -1055,7 +1056,7 @@ pub mod play {
UpdateEntityRotation, UpdateEntityRotation,
MoveVehicle, MoveVehicle,
OpenBook, OpenBook,
OpenScreen, OpenScreen<'a>,
OpenSignEditor, OpenSignEditor,
PingPlay, PingPlay,
PlaceGhostRecipe<'a>, PlaceGhostRecipe<'a>,
@ -1063,8 +1064,8 @@ pub mod play {
PlayerChatMessage<'a>, PlayerChatMessage<'a>,
EndCombat, EndCombat,
EnterCombat, EnterCombat,
CombatDeath, CombatDeath<'a>,
PlayerInfoRemove, PlayerInfoRemove<'a>,
PlayerInfoUpdate<'a>, PlayerInfoUpdate<'a>,
LookAt, LookAt,
SynchronizePlayerPosition, SynchronizePlayerPosition,
@ -1077,7 +1078,7 @@ pub mod play {
UpdateSectionBlocks, UpdateSectionBlocks,
SelectAdvancementsTab<'a>, SelectAdvancementsTab<'a>,
ServerData<'a>, ServerData<'a>,
SetActionBarText, SetActionBarText<'a>,
SetBorderCenter, SetBorderCenter,
SetBorderLerpSize, SetBorderLerpSize,
SetBorderSize, SetBorderSize,
@ -1100,15 +1101,15 @@ pub mod play {
UpdateTeams<'a>, UpdateTeams<'a>,
UpdateScore<'a>, UpdateScore<'a>,
SetSimulationDistance, SetSimulationDistance,
SetSubtitleText, SetSubtitleText<'a>,
UpdateTime, UpdateTime,
SetTitleText, SetTitleText<'a>,
SetTitleAnimationTimes, SetTitleAnimationTimes,
EntitySoundEffect, EntitySoundEffect,
SoundEffect<'a>, SoundEffect<'a>,
StopSound<'a>, StopSound<'a>,
SystemChatMessage, SystemChatMessage<'a>,
SetTabListHeaderAndFooter, SetTabListHeaderAndFooter<'a>,
TagQueryResponse, TagQueryResponse,
PickupItem, PickupItem,
TeleportEntity, TeleportEntity,

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::io::Write; use std::io::Write;
use crate::{Decode, DecodePacket, Encode, EncodePacket, Text, VarInt}; use crate::{Decode, DecodePacket, Encode, EncodePacket, Text, VarInt};
@ -8,18 +9,18 @@ pub struct MapData<'a> {
pub map_id: VarInt, pub map_id: VarInt,
pub scale: i8, pub scale: i8,
pub locked: bool, pub locked: bool,
pub icons: Option<Vec<Icon>>, pub icons: Option<Vec<Icon<'a>>>,
pub data: Option<Data<'a>>, pub data: Option<Data<'a>>,
} }
#[derive(Clone, PartialEq, Debug, Encode, Decode)] #[derive(Clone, PartialEq, Debug, Encode, Decode)]
pub struct Icon { pub struct Icon<'a> {
pub icon_type: IconType, pub icon_type: IconType,
/// In map coordinates; -128 for furthest left, +127 for furthest right /// In map coordinates; -128 for furthest left, +127 for furthest right
pub position: [i8; 2], pub position: [i8; 2],
/// 0 is a vertical icon and increments by 22.5° /// 0 is a vertical icon and increments by 22.5°
pub direction: i8, pub direction: i8,
pub display_name: Option<Text>, pub display_name: Option<Cow<'a, Text>>,
} }
#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] #[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 map_id = VarInt::decode(r)?;
let scale = i8::decode(r)?; let scale = i8::decode(r)?;
let locked = bool::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 columns = u8::decode(r)?;
let data = if columns > 0 { let data = if columns > 0 {

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::io::Write; use std::io::Write;
use crate::packets::s2c::message_signature::MessageSignature; use crate::packets::s2c::message_signature::MessageSignature;
@ -13,12 +14,12 @@ pub struct PlayerChatMessage<'a> {
pub time_stamp: u64, pub time_stamp: u64,
pub salt: u64, pub salt: u64,
pub previous_messages: Vec<MessageSignature<'a>>, 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: MessageFilterType,
pub filter_type_bits: Option<u8>, pub filter_type_bits: Option<u8>,
pub chat_type: VarInt, pub chat_type: VarInt,
pub network_name: Text, pub network_name: Cow<'a, Text>,
pub network_target_name: Option<Text>, pub network_target_name: Option<Cow<'a, Text>>,
} }
#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] #[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 time_stamp = u64::decode(r)?;
let salt = u64::decode(r)?; let salt = u64::decode(r)?;
let previous_messages = Vec::<MessageSignature>::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 = MessageFilterType::decode(r)?;
let filter_type_bits = match filter_type { 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 chat_type = VarInt::decode(r)?;
let network_name = Text::decode(r)?; let network_name = <Cow<'a, Text>>::decode(r)?;
let network_target_name = Option::<Text>::decode(r)?; let network_target_name = Option::<Cow<'a, Text>>::decode(r)?;
Ok(Self { Ok(Self {
sender, sender,

View file

@ -1,16 +1,17 @@
use std::borrow::Cow;
use std::io::Write; use std::io::Write;
use bitfield_struct::bitfield; use bitfield_struct::bitfield;
use uuid::Uuid; use uuid::Uuid;
use crate::types::{GameMode, SignedProperty}; use crate::types::{GameMode, Property};
use crate::{Decode, DecodePacket, Encode, EncodePacket, Text, VarInt}; use crate::{Decode, DecodePacket, Encode, EncodePacket, Text, VarInt};
#[derive(Clone, Debug, EncodePacket, DecodePacket)] #[derive(Clone, Debug, EncodePacket, DecodePacket)]
#[packet_id = 0x36] #[packet_id = 0x36]
pub struct PlayerInfoUpdate<'a> { pub struct PlayerInfoUpdate<'a> {
pub actions: Actions, pub actions: Actions,
pub entries: Vec<Entry<'a>>, pub entries: Cow<'a, [Entry<'a>]>,
} }
#[bitfield(u8)] #[bitfield(u8)]
@ -29,12 +30,12 @@ pub struct Actions {
pub struct Entry<'a> { pub struct Entry<'a> {
pub player_uuid: Uuid, pub player_uuid: Uuid,
pub username: &'a str, pub username: &'a str,
pub properties: Vec<SignedProperty<'a>>, pub properties: Cow<'a, [Property]>,
pub chat_data: Option<ChatData<'a>>, pub chat_data: Option<ChatData<'a>>,
pub listed: bool, pub listed: bool,
pub ping: i32, pub ping: i32,
pub game_mode: GameMode, pub game_mode: GameMode,
pub display_name: Option<Text>, pub display_name: Option<Cow<'a, Text>>,
} }
#[derive(Clone, PartialEq, Debug, Encode, Decode)] #[derive(Clone, PartialEq, Debug, Encode, Decode)]
@ -53,7 +54,7 @@ impl<'a> Encode for PlayerInfoUpdate<'a> {
// Write number of entries. // Write number of entries.
VarInt(self.entries.len() as i32).encode(&mut w)?; 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)?; entry.player_uuid.encode(&mut w)?;
if self.actions.add_player() { if self.actions.add_player() {
@ -126,6 +127,9 @@ impl<'a> Decode<'a> for PlayerInfoUpdate<'a> {
entries.push(entry); entries.push(entry);
} }
Ok(Self { actions, entries }) Ok(Self {
actions,
entries: entries.into(),
})
} }
} }

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::io::Write; use std::io::Write;
use crate::{Decode, DecodePacket, Encode, EncodePacket, Ident, ItemStack, Text, VarInt}; use crate::{Decode, DecodePacket, Encode, EncodePacket, Ident, ItemStack, Text, VarInt};
@ -26,8 +27,8 @@ pub struct AdvancementRequirements<'a> {
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub struct AdvancementDisplay<'a> { pub struct AdvancementDisplay<'a> {
pub title: Text, pub title: Cow<'a, Text>,
pub description: Text, pub description: Cow<'a, Text>,
pub icon: Option<ItemStack>, pub icon: Option<ItemStack>,
pub frame_type: VarInt, pub frame_type: VarInt,
pub flags: i32, pub flags: i32,
@ -66,8 +67,8 @@ impl Encode for AdvancementDisplay<'_> {
impl<'a> Decode<'a> for AdvancementDisplay<'a> { impl<'a> Decode<'a> for AdvancementDisplay<'a> {
fn decode(r: &mut &'a [u8]) -> anyhow::Result<Self> { fn decode(r: &mut &'a [u8]) -> anyhow::Result<Self> {
let title = Text::decode(r)?; let title = <Cow<'a, Text>>::decode(r)?;
let description = Text::decode(r)?; let description = <Cow<'a, Text>>::decode(r)?;
let icon = Option::<ItemStack>::decode(r)?; let icon = Option::<ItemStack>::decode(r)?;
let frame_type = VarInt::decode(r)?; let frame_type = VarInt::decode(r)?;
let flags = i32::decode(r)?; let flags = i32::decode(r)?;

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::io::Write; use std::io::Write;
use anyhow::bail; use anyhow::bail;
@ -8,24 +9,24 @@ use crate::{Decode, Encode, Text};
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub enum UpdateTeamsMode<'a> { pub enum UpdateTeamsMode<'a> {
CreateTeam { CreateTeam {
team_display_name: Text, team_display_name: Cow<'a, Text>,
friendly_flags: TeamFlags, friendly_flags: TeamFlags,
name_tag_visibility: NameTagVisibility, name_tag_visibility: NameTagVisibility,
collision_rule: CollisionRule, collision_rule: CollisionRule,
team_color: TeamColor, team_color: TeamColor,
team_prefix: Text, team_prefix: Cow<'a, Text>,
team_suffix: Text, team_suffix: Cow<'a, Text>,
entities: Vec<&'a str>, entities: Vec<&'a str>,
}, },
RemoveTeam, RemoveTeam,
UpdateTeamInfo { UpdateTeamInfo {
team_display_name: Text, team_display_name: Cow<'a, Text>,
friendly_flags: TeamFlags, friendly_flags: TeamFlags,
name_tag_visibility: NameTagVisibility, name_tag_visibility: NameTagVisibility,
collision_rule: CollisionRule, collision_rule: CollisionRule,
team_color: TeamColor, team_color: TeamColor,
team_prefix: Text, team_prefix: Cow<'a, Text>,
team_suffix: Text, team_suffix: Cow<'a, Text>,
}, },
AddEntities { AddEntities {
entities: Vec<&'a str>, entities: Vec<&'a str>,

View file

@ -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 { impl fmt::Debug for Text {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.write_string(f) self.write_string(f)

View file

@ -137,17 +137,10 @@ pub enum StructureBlockRotation {
} }
#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode, Serialize, Deserialize)] #[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode, Serialize, Deserialize)]
pub struct SignedProperty<'a> { pub struct Property<S = String> {
pub name: &'a str, pub name: S,
pub value: &'a str, pub value: S,
pub signature: Option<&'a str>, pub signature: Option<S>,
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, Serialize, Deserialize)]
pub struct SignedPropertyOwned {
pub name: String,
pub value: String,
pub signature: Option<String>,
} }
#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] #[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)]
@ -366,6 +359,34 @@ pub struct Statistic {
pub value: VarInt, 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)] #[bitfield(u8)]
#[derive(PartialEq, Eq, Encode, Decode)] #[derive(PartialEq, Eq, Encode, Decode)]
pub struct EntityEffectFlags { pub struct EntityEffectFlags {

View file

@ -9,7 +9,7 @@ use anyhow::anyhow;
use serde::de::Error as _; use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize}; 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 /// A newtype wrapper around a string type `S` which guarantees the wrapped
/// string meets the criteria for a valid Minecraft username. /// 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> impl<S> fmt::Display for Username<S>
where where
S: AsRef<str>, S: AsRef<str>,

View file

@ -1,5 +1,5 @@
[package] [package]
name = "valence_derive" name = "valence_protocol_macros"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

View file

@ -10,7 +10,7 @@ version = project.mod_version
group = project.maven_group group = project.maven_group
dependencies { 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 // To change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}" minecraft "com.mojang:minecraft:${project.minecraft_version}"

View file

@ -1,16 +1,13 @@
# Done to increase the memory available to gradle. # Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G org.gradle.jvmargs=-Xmx1G
# Fabric Properties # Fabric Properties
# check these on https://fabricmc.net/develop # check these on https://fabricmc.net/develop
minecraft_version=1.19.3 minecraft_version=1.19.3
yarn_mappings=1.19.3+build.3 yarn_mappings=1.19.3+build.3
loader_version=0.14.11 loader_version=0.14.11
# Mod Properties # Mod Properties
mod_version = 1.0.0 mod_version=1.0.0
maven_group = dev.00a maven_group=dev.00a
archives_base_name = valence-extractor archives_base_name=valence-extractor
# Dependencies # Dependencies
fabric_version=0.69.1+1.19.3 fabric_version=0.73.2+1.19.3