mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 14:31:30 +11:00
Reorganize Project (#321)
## Description - `valence` and `valence_protocol` have been divided into smaller crates in order to parallelize the build and improve IDE responsiveness. In the process, code architecture has been made clearer by removing circular dependencies between modules. `valence` is now just a shell around the other crates. - `workspace.packages` and `workspace.dependencies` are now used. This makes dependency managements and crate configuration much easier. - `valence_protocol` is no more. Most things from `valence_protocol` ended up in `valence_core`. We won't advertise `valence_core` as a general-purpose protocol library since it contains too much valence-specific stuff. Closes #308. - Networking code (login, initial TCP connection handling, etc.) has been extracted into the `valence_network` crate. The API has been expanded and improved with better defaults. Player counts and initial connections to the server are now tracked separately. Player counts function by default without any user configuration. - Some crates like `valence_anvil`, `valence_network`, `valence_player_list`, `valence_inventory`, etc. are now optional. They can be enabled/disabled with feature flags and `DefaultPlugins` just like bevy. - Whole-server unit tests have been moved to `valence/src/tests` in order to avoid [cyclic dev-dependencies](https://github.com/rust-lang/cargo/issues/4242). - Tools like `valence_stresser` and `packet_inspector` have been moved to a new `tools` directory. Renamed `valence_stresser` to `stresser`. Closes #241. - Moved all benches to `valence/benches/` to make them easier to run and organize. Ignoring transitive dependencies and `valence_core`, here's what the dependency graph looks like now: ```mermaid graph TD network --> client client --> instance biome --> registry dimension --> registry instance --> biome instance --> dimension instance --> entity player_list --> client inventory --> client anvil --> instance entity --> block ``` ### Issues - Inventory tests inspect many private implementation details of the inventory module, forcing us to mark things as `pub` and `#[doc(hidden)]`. It would be ideal if the tests only looked at observable behavior. - Consider moving packets in `valence_core` elsewhere. `Particle` wants to use `BlockState`, but that's defined in `valence_block`, so we can't use it without causing cycles. - Unsure what exactly should go in `valence::prelude`. - This could use some more tests of course, but I'm holding off on that until I'm confident this is the direction we want to take things. ## TODOs - [x] Update examples. - [x] Update benches. - [x] Update main README. - [x] Add short READMEs to crates. - [x] Test new schedule to ensure behavior is the same. - [x] Update tools. - [x] Copy lints to all crates. - [x] Fix docs, clippy, etc.
This commit is contained in:
parent
1db779136c
commit
eaf1e18610
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
|||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
- run: cp crates/playground/src/playground.template.rs crates/playground/src/playground.rs
|
||||
- run: cp tools/playground/src/playground.template.rs tools/playground/src/playground.rs
|
||||
- name: Set up cargo cache
|
||||
uses: actions/cache@v3
|
||||
continue-on-error: false
|
||||
|
|
|
@ -24,7 +24,7 @@ Playgrounds are meant to provide a quick and minimal environment to test out new
|
|||
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
|
||||
cp tools/playground/src/playground.template.rs tools/playground/src/playground.rs
|
||||
```
|
||||
|
||||
Make your changes to `crates/playground/src/playground.rs`. To run it:
|
||||
|
|
88
Cargo.toml
88
Cargo.toml
|
@ -1,8 +1,94 @@
|
|||
[workspace]
|
||||
members = ["crates/*"]
|
||||
members = ["crates/*", "tools/*"]
|
||||
exclude = ["rust-mc-bot"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.0-dev+mc.1.19.4"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/valence-rs/valence"
|
||||
documentation = "https://docs.rs/valence/"
|
||||
license = "MIT"
|
||||
|
||||
[workspace.dependencies]
|
||||
aes = "0.7.5"
|
||||
anyhow = "1.0.70"
|
||||
approx = "0.5.1"
|
||||
arrayvec = "0.7.2"
|
||||
async-trait = "0.1.60"
|
||||
atty = "0.2.14"
|
||||
base64 = "0.21.0"
|
||||
bevy_app = { version = "0.10.1", default-features = false }
|
||||
bevy_ecs = { version = "0.10.1", default-features = false, features = ["trace"] }
|
||||
bevy_mod_debugdump = "0.7.0"
|
||||
bitfield-struct = "0.3.1"
|
||||
byteorder = "1.4.3"
|
||||
bytes = "1.2.1"
|
||||
cesu8 = "1.1.0"
|
||||
cfb8 = "0.7.1"
|
||||
clap = { version = "4.0.30", features = ["derive"] }
|
||||
criterion = "0.4.0"
|
||||
directories = "5.0.0"
|
||||
eframe = { version = "0.21.0", default-features = false }
|
||||
egui = "0.21.0"
|
||||
enum-map = "2.4.2"
|
||||
flate2 = "1.0.24"
|
||||
flume = "0.10.14"
|
||||
fs_extra = "1.2.0"
|
||||
glam = "0.23.0"
|
||||
heck = "0.4.0"
|
||||
hmac = "0.12.1"
|
||||
indexmap = "1.9.3"
|
||||
noise = "0.8.2"
|
||||
num = "0.4.0"
|
||||
num-bigint = "0.4.3"
|
||||
num-integer = "0.1.45"
|
||||
owo-colors = "3.5.0"
|
||||
parking_lot = "0.12.1"
|
||||
paste = "1.0.11"
|
||||
proc-macro2 = "1.0.56"
|
||||
quote = "1.0.26"
|
||||
rand = "0.8.5"
|
||||
rayon = "1.7.0"
|
||||
regex = "1.6.0"
|
||||
reqwest = { version = "0.11.12", default-features = false }
|
||||
rfd = "0.11.3"
|
||||
rsa = "0.7.2"
|
||||
rsa-der = "0.3.0"
|
||||
rustc-hash = "1.1.0"
|
||||
serde = "1.0.160"
|
||||
serde_json = "1.0.96"
|
||||
sha1 = "0.10.5"
|
||||
sha2 = "0.10.6"
|
||||
syn = "1.0.109" # TODO: update this.
|
||||
syntect = { version = "5.0.0", default-features = false }
|
||||
tempfile = "3.3.0"
|
||||
thiserror = "1.0.40"
|
||||
time = "0.3.17"
|
||||
tokio = { version = "1.27.0", features = ["full"] }
|
||||
toml = "0.7.2"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = "0.3.16"
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
||||
uuid = "1.3.1"
|
||||
valence_anvil.path = "crates/valence_anvil"
|
||||
valence_biome.path = "crates/valence_biome"
|
||||
valence_block.path = "crates/valence_block"
|
||||
valence_build_utils.path = "crates/valence_build_utils"
|
||||
valence_client.path = "crates/valence_client"
|
||||
valence_core_macros.path = "crates/valence_core_macros"
|
||||
valence_core.path = "crates/valence_core"
|
||||
valence_dimension.path = "crates/valence_dimension"
|
||||
valence_entity.path = "crates/valence_entity"
|
||||
valence_instance.path = "crates/valence_instance"
|
||||
valence_inventory.path = "crates/valence_inventory"
|
||||
valence_nbt = { path = "crates/valence_nbt", features = ["uuid"] }
|
||||
valence_network.path = "crates/valence_network"
|
||||
valence_player_list.path = "crates/valence_player_list"
|
||||
valence_registry.path = "crates/valence_registry"
|
||||
valence.path = "crates/valence"
|
||||
zip = "0.6.3"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<p align="center">
|
||||
<img src="assets/logo-full.svg" width="650" align="center">
|
||||
<img src="https://raw.githubusercontent.com/valence-rs/valence/main/assets/logo-full.svg" width="650" align="center">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
@ -27,6 +27,8 @@ Opinionated features like dynamic scripting, dedicated executables, and vanilla
|
|||
built as optional plugins. This level of modularity is desirable for those looking to build highly custom experiences
|
||||
in Minecraft such as minigame servers.
|
||||
|
||||
⚠️ **Valence is still early in development with many features unimplemented or incomplete. Expect to encounter bugs, limitations, and breaking changes.**
|
||||
|
||||
# Goals
|
||||
|
||||
Valence aims to be the following:
|
||||
|
@ -45,11 +47,8 @@ Valence aims to be the following:
|
|||
|
||||
## Current Status
|
||||
|
||||
⚠️ **Valence is still early in development with many features unimplemented or incomplete. Expect to encounter bugs, limitations, and breaking changes.** Here are some noteworthy achievements:
|
||||
|
||||
Here are some noteworthy achievements:
|
||||
- `valence_nbt`: A speedy new library for Minecraft's Named Binary Tag (NBT) format.
|
||||
- `valence_protocol`: A library for working with Minecraft's protocol. Does not depend on Valence and can be used in
|
||||
other projects.
|
||||
- Authentication, encryption, and compression
|
||||
- Block states
|
||||
- Chunks
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
[package]
|
||||
name = "dump_schedule"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bevy_mod_debugdump = "0.7.0"
|
||||
valence = { path = "../valence", version = "0.2.0" }
|
|
@ -1,37 +0,0 @@
|
|||
[package]
|
||||
name = "packet_inspector"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "A simple Minecraft proxy for inspecting packets."
|
||||
|
||||
[features]
|
||||
default = ["syntax_highlighting"]
|
||||
syntax_highlighting = ["syntect"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4.0.30", features = ["derive"] }
|
||||
regex = "1.6.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing-subscriber = "0.3.16"
|
||||
valence_protocol = { path = "../valence_protocol", version = "0.1.0", features = [
|
||||
"compression",
|
||||
] }
|
||||
|
||||
egui = "0.21.0"
|
||||
eframe = { version = "0.21.0", default-features = false, features = [
|
||||
"default_fonts", # Embed the default egui fonts.
|
||||
"glow", # Use the glow rendering backend. Alternative: "wgpu".
|
||||
] }
|
||||
enum-map = "2.4.2"
|
||||
syntect = { version = "5", optional = true, default-features = false, features = [
|
||||
"default-fancy",
|
||||
] }
|
||||
time = { version = "0.3.17", features = ["local-offset"] }
|
||||
rfd = "0.11"
|
||||
owo-colors = "3.5.0"
|
||||
atty = "0.2"
|
||||
directories = "4.0"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
toml = "0.7.2"
|
||||
valence_nbt = { path = "../valence_nbt", version = "0.5.0", features = ["preserve_order"] }
|
|
@ -1,16 +0,0 @@
|
|||
use tracing::Level;
|
||||
use valence::bevy_app::App;
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod extras;
|
||||
mod playground;
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(Level::DEBUG)
|
||||
.init();
|
||||
|
||||
let mut app = App::new();
|
||||
playground::build_app(&mut app);
|
||||
app.run();
|
||||
}
|
|
@ -1,71 +1,61 @@
|
|||
[package]
|
||||
name = "valence"
|
||||
version = "0.2.0+mc1.19.4"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "A framework for building Minecraft servers in Rust."
|
||||
repository = "https://github.com/rj00a/valence"
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
license.workspace = true
|
||||
keywords = ["minecraft", "gamedev", "server", "ecs"]
|
||||
categories = ["game-engines"]
|
||||
build = "build/main.rs"
|
||||
authors = ["Ryan Johnson <ryanj00a@gmail.com>"]
|
||||
|
||||
[features]
|
||||
default = ["network", "player_list", "inventory", "anvil"]
|
||||
network = ["dep:valence_network"]
|
||||
player_list = ["dep:valence_player_list"]
|
||||
inventory = ["dep:valence_inventory"]
|
||||
anvil = ["dep:valence_anvil"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.65"
|
||||
arrayvec = "0.7.2"
|
||||
async-trait = "0.1.60"
|
||||
base64 = "0.21.0"
|
||||
bevy_app = "0.10"
|
||||
bevy_ecs = "0.10"
|
||||
bitfield-struct = "0.3.1"
|
||||
bytes = "1.2.1"
|
||||
flume = "0.10.14"
|
||||
glam = "0.23.0"
|
||||
hmac = "0.12.1"
|
||||
num = "0.4.0"
|
||||
parking_lot = "0.12.1"
|
||||
paste = "1.0.11"
|
||||
rand = "0.8.5"
|
||||
rsa = "0.7.2"
|
||||
rsa-der = "0.3.0"
|
||||
rustc-hash = "1.1.0"
|
||||
serde = { version = "1.0.145", features = ["derive"] }
|
||||
serde_json = "1.0.85"
|
||||
sha1 = "0.10.5"
|
||||
sha2 = "0.10.6"
|
||||
thiserror = "1.0.35"
|
||||
tokio = { version = "1.25.0", features = ["full"] }
|
||||
tracing = "0.1.37"
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
||||
uuid = { version = "1.1.2", features = ["serde"] }
|
||||
valence_nbt = { version = "0.5.0", path = "../valence_nbt", features = [
|
||||
"uuid",
|
||||
] }
|
||||
valence_protocol = { version = "0.1.0", path = "../valence_protocol", features = [
|
||||
"encryption",
|
||||
"compression",
|
||||
"glam",
|
||||
] }
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.11.12"
|
||||
default-features = false
|
||||
# Avoid OpenSSL dependency on Linux.
|
||||
features = ["rustls-tls", "json"]
|
||||
bevy_app.workspace = true
|
||||
bevy_ecs.workspace = true
|
||||
glam.workspace = true
|
||||
uuid.workspace = true
|
||||
valence_nbt.workspace = true
|
||||
valence_core.workspace = true
|
||||
valence_registry.workspace = true
|
||||
valence_block.workspace = true
|
||||
valence_biome.workspace = true
|
||||
valence_dimension.workspace = true
|
||||
valence_entity.workspace = true
|
||||
valence_instance.workspace = true
|
||||
valence_client.workspace = true
|
||||
valence_network = { workspace = true, optional = true }
|
||||
valence_player_list = { workspace = true, optional = true }
|
||||
valence_inventory = { workspace = true, optional = true }
|
||||
valence_anvil = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
glam = { version = "0.23.0", features = ["approx"] }
|
||||
noise = "0.8.2"
|
||||
tracing-subscriber = "0.3.16"
|
||||
anyhow.workspace = true
|
||||
bytes.workspace = true
|
||||
noise.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
rand.workspace = true
|
||||
tracing.workspace = true
|
||||
flume.workspace = true
|
||||
clap.workspace = true
|
||||
criterion.workspace = true
|
||||
fs_extra.workspace = true
|
||||
tempfile.workspace = true
|
||||
zip.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.65"
|
||||
heck = "0.4.0"
|
||||
proc-macro2 = "1.0.43"
|
||||
quote = "1.0.21"
|
||||
serde = { version = "1.0.145", features = ["derive"] }
|
||||
serde_json = "1.0.85"
|
||||
rayon = "1.5.3"
|
||||
num = "0.4.0"
|
||||
[dev-dependencies.reqwest]
|
||||
workspace = true
|
||||
default-features = false
|
||||
# Avoid OpenSSL dependency on Linux.
|
||||
features = ["rustls-tls", "blocking", "stream"]
|
||||
|
||||
[[bench]]
|
||||
name = "main"
|
||||
harness = false
|
||||
|
|
5
crates/valence/README.md
Normal file
5
crates/valence/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# valence
|
||||
|
||||
The main Valence crate. This acts primarily as a shell to re-export the other crates and consolidate plugins. Users are expected to use the other crates indirectly through this interface.
|
||||
|
||||
Additionally, project examples and whole-server unit tests are stored in `examples/` and `src/tests/` respectively.
|
|
@ -1,18 +1,16 @@
|
|||
use std::fs::create_dir_all;
|
||||
use std::hint::black_box;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::Criterion;
|
||||
use fs_extra::dir::CopyOptions;
|
||||
use reqwest::IntoUrl;
|
||||
use valence::anvil::AnvilWorld;
|
||||
use valence::instance::Chunk;
|
||||
use valence_anvil::AnvilWorld;
|
||||
use zip::ZipArchive;
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
pub fn load(c: &mut Criterion) {
|
||||
let world_dir = get_world_asset(
|
||||
"https://github.com/valence-rs/valence-test-data/archive/refs/heads/asset/sp_world_1.19.2.zip",
|
||||
"1.19.2 benchmark world",
|
||||
|
@ -21,7 +19,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
|||
|
||||
let mut world = AnvilWorld::new(world_dir);
|
||||
|
||||
c.bench_function("Load square 10x10", |b| {
|
||||
c.bench_function("anvil_load_10x10", |b| {
|
||||
b.iter(|| {
|
||||
let world = black_box(&mut world);
|
||||
|
89
crates/valence/benches/block.rs
Normal file
89
crates/valence/benches/block.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use std::hint::black_box;
|
||||
|
||||
use criterion::Criterion;
|
||||
use valence::block::{BlockKind, BlockState, PropName, PropValue};
|
||||
use valence::item::ItemKind;
|
||||
|
||||
pub fn block(c: &mut Criterion) {
|
||||
let states = BlockKind::ALL.map(BlockKind::to_state);
|
||||
|
||||
c.bench_function("BlockState::from_kind", |b| {
|
||||
b.iter(|| {
|
||||
for kind in black_box(BlockKind::ALL) {
|
||||
black_box(BlockState::from_kind(kind));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::to_kind", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.to_kind());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::get", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.get(PropName::Note));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::set", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.set(PropName::Note, PropValue::Didgeridoo));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::is_liquid", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.is_liquid());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::is_opaque", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.is_opaque());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::is_replaceable", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.is_replaceable());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::luminance", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.luminance());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("BlockKind::to_item_kind", |b| {
|
||||
b.iter(|| {
|
||||
for kind in black_box(BlockKind::ALL) {
|
||||
black_box(kind.to_item_kind());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockKind::from_item_kind", |b| {
|
||||
b.iter(|| {
|
||||
for kind in black_box(ItemKind::ALL) {
|
||||
black_box(BlockKind::from_item_kind(kind));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
27
crates/valence/benches/decode_array.rs
Normal file
27
crates/valence/benches/decode_array.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use std::hint::black_box;
|
||||
|
||||
use criterion::Criterion;
|
||||
use valence::packet::{Decode, Encode};
|
||||
|
||||
pub fn decode_array(c: &mut Criterion) {
|
||||
let floats = [123.0, 456.0, 789.0];
|
||||
let mut buf = [0u8; 24];
|
||||
|
||||
floats.encode(buf.as_mut_slice()).unwrap();
|
||||
|
||||
c.bench_function("<[f64; 3]>::decode", |b| {
|
||||
b.iter(|| {
|
||||
let mut r = black_box(buf.as_slice());
|
||||
let _ = black_box(<[f64; 3]>::decode(&mut r));
|
||||
});
|
||||
});
|
||||
|
||||
let bytes = [42; 4096];
|
||||
|
||||
c.bench_function("<[u8; 4096]>::decode", |b| {
|
||||
b.iter(|| {
|
||||
let mut r = black_box(bytes.as_slice());
|
||||
let _ = black_box(<[u8; 4096]>::decode(&mut r));
|
||||
})
|
||||
});
|
||||
}
|
20
crates/valence/benches/main.rs
Normal file
20
crates/valence/benches/main.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use criterion::{criterion_group, criterion_main};
|
||||
|
||||
mod anvil;
|
||||
mod block;
|
||||
mod decode_array;
|
||||
mod packet;
|
||||
mod var_int;
|
||||
mod var_long;
|
||||
|
||||
criterion_group! {
|
||||
benches,
|
||||
anvil::load,
|
||||
block::block,
|
||||
decode_array::decode_array,
|
||||
packet::packet,
|
||||
var_int::var_int,
|
||||
var_long::var_long,
|
||||
}
|
||||
|
||||
criterion_main!(benches);
|
|
@ -1,114 +1,18 @@
|
|||
use std::borrow::Cow;
|
||||
use std::time::Duration;
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use rand::Rng;
|
||||
use valence_nbt::{compound, List};
|
||||
use valence_protocol::array::LengthPrefixedArray;
|
||||
use valence_protocol::block::{BlockKind, BlockState, PropName, PropValue};
|
||||
use valence_protocol::byte_angle::ByteAngle;
|
||||
use valence_protocol::decoder::{decode_packet, PacketDecoder};
|
||||
use valence_protocol::encoder::{encode_packet, encode_packet_compressed, PacketEncoder};
|
||||
use valence_protocol::item::ItemKind;
|
||||
use valence_protocol::packet::s2c::play::{ChunkDataS2c, EntitySpawnS2c, PlayerListHeaderS2c};
|
||||
use valence_protocol::text::{Color, TextFormat};
|
||||
use valence_protocol::var_int::VarInt;
|
||||
use valence_protocol::var_long::VarLong;
|
||||
use valence_protocol::{Decode, Encode};
|
||||
use criterion::Criterion;
|
||||
use valence::nbt::{compound, List};
|
||||
use valence::packet::array::LengthPrefixedArray;
|
||||
use valence::packet::byte_angle::ByteAngle;
|
||||
use valence::packet::decode::{decode_packet, PacketDecoder};
|
||||
use valence::packet::encode::{encode_packet, encode_packet_compressed, PacketEncoder};
|
||||
use valence::packet::s2c::play::{ChunkDataS2c, EntitySpawnS2c, PlayerListHeaderS2c};
|
||||
use valence::packet::var_int::VarInt;
|
||||
use valence::prelude::*;
|
||||
use valence::text::TextFormat;
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = Criterion::default()
|
||||
.measurement_time(Duration::from_secs(5)).confidence_level(0.99);
|
||||
targets = blocks, packets, var_int, var_long, decode_array
|
||||
}
|
||||
criterion_main!(benches);
|
||||
|
||||
fn blocks(c: &mut Criterion) {
|
||||
let states = BlockKind::ALL.map(BlockKind::to_state);
|
||||
|
||||
c.bench_function("BlockState::from_kind", |b| {
|
||||
b.iter(|| {
|
||||
for kind in black_box(BlockKind::ALL) {
|
||||
black_box(BlockState::from_kind(kind));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::to_kind", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.to_kind());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::get", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.get(PropName::Note));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::set", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.set(PropName::Note, PropValue::Didgeridoo));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::is_liquid", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.is_liquid());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::is_opaque", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.is_opaque());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::is_replaceable", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.is_replaceable());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("BlockState::luminance", |b| {
|
||||
b.iter(|| {
|
||||
for state in black_box(states) {
|
||||
black_box(state.luminance());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("BlockKind::to_item_kind", |b| {
|
||||
b.iter(|| {
|
||||
for kind in black_box(BlockKind::ALL) {
|
||||
black_box(kind.to_item_kind());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("BlockKind::from_item_kind", |b| {
|
||||
b.iter(|| {
|
||||
for kind in black_box(ItemKind::ALL) {
|
||||
black_box(BlockKind::from_item_kind(kind));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn packets(c: &mut Criterion) {
|
||||
pub fn packet(c: &mut Criterion) {
|
||||
let mut encoder = PacketEncoder::new();
|
||||
|
||||
const BLOCKS_AND_BIOMES: [u8; 2000] = [0x80; 2000];
|
||||
|
@ -116,8 +20,7 @@ fn packets(c: &mut Criterion) {
|
|||
[LengthPrefixedArray([0xff; 2048]); 26];
|
||||
|
||||
let chunk_data_packet = ChunkDataS2c {
|
||||
chunk_x: 123,
|
||||
chunk_z: 456,
|
||||
pos: ChunkPos::new(123, 456),
|
||||
heightmaps: Cow::Owned(compound! {
|
||||
"MOTION_BLOCKING" => List::Long(vec![123; 256]),
|
||||
}),
|
||||
|
@ -146,7 +49,7 @@ fn packets(c: &mut Criterion) {
|
|||
entity_id: VarInt(1234),
|
||||
object_uuid: Default::default(),
|
||||
kind: VarInt(5),
|
||||
position: [123.0, 456.0, 789.0],
|
||||
position: DVec3::new(123.0, 456.0, 789.0),
|
||||
pitch: ByteAngle(200),
|
||||
yaw: ByteAngle(100),
|
||||
head_yaw: ByteAngle(50),
|
||||
|
@ -320,86 +223,3 @@ fn packets(c: &mut Criterion) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn var_int(c: &mut Criterion) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
c.bench_function("VarInt::encode", |b| {
|
||||
b.iter_with_setup(
|
||||
|| rng.gen(),
|
||||
|i| {
|
||||
let i: i32 = black_box(i);
|
||||
|
||||
let mut buf = [0; VarInt::MAX_SIZE];
|
||||
let _ = black_box(VarInt(i).encode(buf.as_mut_slice()));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
c.bench_function("VarInt::decode", |b| {
|
||||
b.iter_with_setup(
|
||||
|| {
|
||||
let mut buf = [0; VarInt::MAX_SIZE];
|
||||
VarInt(rng.gen()).encode(buf.as_mut_slice()).unwrap();
|
||||
buf
|
||||
},
|
||||
|buf| {
|
||||
let mut r = black_box(buf.as_slice());
|
||||
let _ = black_box(VarInt::decode(&mut r));
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn var_long(c: &mut Criterion) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
c.bench_function("VarLong::encode", |b| {
|
||||
b.iter_with_setup(
|
||||
|| rng.gen(),
|
||||
|i| {
|
||||
let i: i64 = black_box(i);
|
||||
|
||||
let mut buf = [0; VarLong::MAX_SIZE];
|
||||
let _ = black_box(VarLong(i).encode(buf.as_mut_slice()));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
c.bench_function("VarLong::decode", |b| {
|
||||
b.iter_with_setup(
|
||||
|| {
|
||||
let mut buf = [0; VarLong::MAX_SIZE];
|
||||
VarLong(rng.gen()).encode(buf.as_mut_slice()).unwrap();
|
||||
buf
|
||||
},
|
||||
|buf| {
|
||||
let mut r = black_box(buf.as_slice());
|
||||
let _ = black_box(VarLong::decode(&mut r));
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn decode_array(c: &mut Criterion) {
|
||||
let floats = [123.0, 456.0, 789.0];
|
||||
let mut buf = [0u8; 24];
|
||||
|
||||
floats.encode(buf.as_mut_slice()).unwrap();
|
||||
|
||||
c.bench_function("<[f64; 3]>::decode", |b| {
|
||||
b.iter(|| {
|
||||
let mut r = black_box(buf.as_slice());
|
||||
let _ = black_box(<[f64; 3]>::decode(&mut r));
|
||||
});
|
||||
});
|
||||
|
||||
let bytes = [42; 4096];
|
||||
|
||||
c.bench_function("<[u8; 4096]>::decode", |b| {
|
||||
b.iter(|| {
|
||||
let mut r = black_box(bytes.as_slice());
|
||||
let _ = black_box(<[u8; 4096]>::decode(&mut r));
|
||||
})
|
||||
});
|
||||
}
|
36
crates/valence/benches/var_int.rs
Normal file
36
crates/valence/benches/var_int.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use std::hint::black_box;
|
||||
|
||||
use criterion::Criterion;
|
||||
use rand::Rng;
|
||||
use valence::packet::var_int::VarInt;
|
||||
use valence::packet::{Decode, Encode};
|
||||
|
||||
pub fn var_int(c: &mut Criterion) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
c.bench_function("VarInt::encode", |b| {
|
||||
b.iter_with_setup(
|
||||
|| rng.gen(),
|
||||
|i| {
|
||||
let i: i32 = black_box(i);
|
||||
|
||||
let mut buf = [0; VarInt::MAX_SIZE];
|
||||
let _ = black_box(VarInt(i).encode(buf.as_mut_slice()));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
c.bench_function("VarInt::decode", |b| {
|
||||
b.iter_with_setup(
|
||||
|| {
|
||||
let mut buf = [0; VarInt::MAX_SIZE];
|
||||
VarInt(rng.gen()).encode(buf.as_mut_slice()).unwrap();
|
||||
buf
|
||||
},
|
||||
|buf| {
|
||||
let mut r = black_box(buf.as_slice());
|
||||
let _ = black_box(VarInt::decode(&mut r));
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
36
crates/valence/benches/var_long.rs
Normal file
36
crates/valence/benches/var_long.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use std::hint::black_box;
|
||||
|
||||
use criterion::Criterion;
|
||||
use rand::Rng;
|
||||
use valence::packet::var_long::VarLong;
|
||||
use valence::packet::{Decode, Encode};
|
||||
|
||||
pub fn var_long(c: &mut Criterion) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
c.bench_function("VarLong::encode", |b| {
|
||||
b.iter_with_setup(
|
||||
|| rng.gen(),
|
||||
|i| {
|
||||
let i: i64 = black_box(i);
|
||||
|
||||
let mut buf = [0; VarLong::MAX_SIZE];
|
||||
let _ = black_box(VarLong(i).encode(buf.as_mut_slice()));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
c.bench_function("VarLong::decode", |b| {
|
||||
b.iter_with_setup(
|
||||
|| {
|
||||
let mut buf = [0; VarLong::MAX_SIZE];
|
||||
VarLong(rng.gen()).encode(buf.as_mut_slice()).unwrap();
|
||||
buf
|
||||
},
|
||||
|buf| {
|
||||
let mut r = black_box(buf.as_slice());
|
||||
let _ = black_box(VarLong::decode(&mut r));
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use heck::ToPascalCase;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ident;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
struct EntityEvents {
|
||||
entity_status: BTreeMap<String, u8>,
|
||||
entity_animation: BTreeMap<String, u8>,
|
||||
}
|
||||
|
||||
pub fn build() -> anyhow::Result<TokenStream> {
|
||||
let entity_events: EntityEvents =
|
||||
serde_json::from_str(include_str!("../../../extracted/misc.json"))?;
|
||||
|
||||
let mut statuses: Vec<_> = entity_events.entity_status.into_iter().collect();
|
||||
statuses.sort_by_key(|(_, id)| *id);
|
||||
|
||||
let mut animations: Vec<_> = entity_events.entity_animation.into_iter().collect();
|
||||
animations.sort_by_key(|(_, id)| *id);
|
||||
|
||||
let entity_status_variants: Vec<_> = statuses
|
||||
.iter()
|
||||
.map(|(name, code)| {
|
||||
let name = ident(name.to_pascal_case());
|
||||
let code = *code as isize;
|
||||
|
||||
quote! {
|
||||
#name = #code,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let entity_animation_variants: Vec<_> = animations
|
||||
.iter()
|
||||
.map(|(name, code)| {
|
||||
let name = ident(name.to_pascal_case());
|
||||
let code = *code as isize;
|
||||
|
||||
quote! {
|
||||
#name = #code,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(quote! {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum EntityStatus {
|
||||
#(#entity_status_variants)*
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum EntityAnimation {
|
||||
#(#entity_animation_variants)*
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::{env, fs};
|
||||
|
||||
use anyhow::Context;
|
||||
use proc_macro2::{Ident, Span};
|
||||
|
||||
mod entity;
|
||||
mod entity_event;
|
||||
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
println!("cargo:rerun-if-changed=../../extracted/");
|
||||
|
||||
let generators = [
|
||||
(entity::build as fn() -> _, "entity.rs"),
|
||||
(entity_event::build, "entity_event.rs"),
|
||||
];
|
||||
|
||||
let out_dir = env::var_os("OUT_DIR").context("can't get OUT_DIR env var")?;
|
||||
|
||||
for (generator, file_name) in generators {
|
||||
let path = Path::new(&out_dir).join(file_name);
|
||||
let code = generator()?.to_string();
|
||||
fs::write(&path, code)?;
|
||||
|
||||
// Format the output for debugging purposes.
|
||||
// Doesn't matter if rustfmt is unavailable.
|
||||
let _ = Command::new("rustfmt").arg(path).output();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ident(s: impl AsRef<str>) -> Ident {
|
||||
let s = s.as_ref().trim();
|
||||
|
||||
match s.as_bytes() {
|
||||
// TODO: check for the other rust keywords.
|
||||
[b'0'..=b'9', ..] | b"type" => Ident::new(&format!("_{s}"), Span::call_site()),
|
||||
_ => Ident::new(s, Span::call_site()),
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@ use std::thread;
|
|||
use clap::Parser;
|
||||
use flume::{Receiver, Sender};
|
||||
use tracing::warn;
|
||||
use valence::anvil::{AnvilChunk, AnvilWorld};
|
||||
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;
|
||||
|
@ -62,7 +62,7 @@ pub fn main() {
|
|||
};
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.insert_resource(game_state)
|
||||
.add_startup_system(setup)
|
||||
.add_systems(
|
|
@ -4,6 +4,7 @@ use std::time::Instant;
|
|||
|
||||
use valence::instance::{Chunk, Instance};
|
||||
use valence::prelude::*;
|
||||
use valence_network::ConnectionMode;
|
||||
|
||||
const SPAWN_Y: i32 = 64;
|
||||
|
||||
|
@ -14,16 +15,20 @@ 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),
|
||||
)
|
||||
.insert_resource(CoreSettings {
|
||||
compression_threshold: None,
|
||||
..Default::default()
|
||||
})
|
||||
.insert_resource(NetworkSettings {
|
||||
connection_mode: ConnectionMode::Offline,
|
||||
max_connections: 50_000,
|
||||
..Default::default()
|
||||
})
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_systems((
|
||||
record_tick_start_time.in_base_set(CoreSet::First),
|
||||
print_tick_time.in_base_set(CoreSet::Last),
|
||||
print_tick_time.in_base_set(CoreSet::LastFlush),
|
||||
init_clients,
|
||||
despawn_disconnected_clients,
|
||||
))
|
||||
|
@ -34,10 +39,15 @@ fn record_tick_start_time(mut commands: Commands) {
|
|||
commands.insert_resource(TickStart(Instant::now()));
|
||||
}
|
||||
|
||||
fn print_tick_time(server: Res<Server>, time: Res<TickStart>, clients: Query<(), With<Client>>) {
|
||||
fn print_tick_time(
|
||||
server: Res<Server>,
|
||||
settings: Res<CoreSettings>,
|
||||
time: Res<TickStart>,
|
||||
clients: Query<(), With<Client>>,
|
||||
) {
|
||||
let tick = server.current_tick();
|
||||
if tick % (server.tps() / 2) == 0 {
|
||||
let client_count = clients.iter().count();
|
||||
if tick % (settings.tick_rate.get() as i64 / 2) == 0 {
|
||||
let client_count = clients.iter().len();
|
||||
|
||||
let millis = time.0.elapsed().as_secs_f32() * 1000.0;
|
||||
println!("Tick={tick}, MSPT={millis:.04}ms, Clients={client_count}");
|
||||
|
|
|
@ -8,7 +8,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_systems((init_clients, despawn_disconnected_clients))
|
||||
.run();
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
use valence::client::misc::{ChatMessage, InteractBlock};
|
||||
use valence::nbt::{compound, List};
|
||||
use valence::prelude::*;
|
||||
use valence::protocol::types::Hand;
|
||||
|
||||
const FLOOR_Y: i32 = 64;
|
||||
const SIGN_POS: [i32; 3] = [3, FLOOR_Y + 1, 2];
|
||||
|
@ -13,7 +12,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_systems((event_handler, init_clients, despawn_disconnected_clients))
|
||||
.run();
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use valence::client::misc::InteractBlock;
|
||||
use valence::client::ClientInventoryState;
|
||||
use valence::inventory::ClientInventoryState;
|
||||
use valence::prelude::*;
|
||||
use valence::protocol::types::Hand;
|
||||
|
||||
const SPAWN_Y: i32 = 64;
|
||||
|
||||
|
@ -11,7 +10,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_system(despawn_disconnected_clients)
|
||||
|
@ -131,7 +130,7 @@ fn place_blocks(
|
|||
continue;
|
||||
};
|
||||
|
||||
let Some(block_kind) = stack.item.to_block_kind() else {
|
||||
let Some(block_kind) = BlockKind::from_item_kind(stack.item) else {
|
||||
// can't place this item as a block
|
||||
continue;
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_systems((toggle_gamemode_on_sneak, open_chest))
|
||||
|
|
|
@ -20,7 +20,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_system(handle_combat_events.in_schedule(EventLoopSchedule))
|
||||
|
|
|
@ -23,7 +23,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup_biomes.before(setup))
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
|
|
|
@ -23,7 +23,7 @@ fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_system(update_sphere)
|
||||
|
@ -77,10 +77,11 @@ fn init_clients(
|
|||
}
|
||||
|
||||
fn update_sphere(
|
||||
settings: Res<CoreSettings>,
|
||||
server: Res<Server>,
|
||||
mut parts: Query<(&mut Position, &mut Look, &mut HeadYaw), With<SpherePart>>,
|
||||
) {
|
||||
let time = server.current_tick() as f64 / server.tps() as f64;
|
||||
let time = server.current_tick() as f64 / settings.tick_rate.get() as f64;
|
||||
|
||||
let rot_angles = DVec3::new(0.2, 0.4, 0.6) * SPHERE_FREQ * time * TAU % TAU;
|
||||
let rot = DQuat::from_euler(EulerRot::XYZ, rot_angles.x, rot_angles.y, rot_angles.z);
|
||||
|
|
|
@ -9,7 +9,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_systems((init_clients, squat_and_die, necromancy))
|
||||
.add_system(despawn_disconnected_clients)
|
||||
|
@ -88,7 +88,7 @@ fn necromancy(
|
|||
// make the client respawn in another instance
|
||||
let idx = instances.iter().position(|i| i == loc.0).unwrap();
|
||||
|
||||
let count = instances.iter().count();
|
||||
let count = instances.iter().len();
|
||||
|
||||
loc.0 = instances.into_iter().nth((idx + 1) % count).unwrap();
|
||||
}
|
||||
|
|
|
@ -5,10 +5,9 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::Rng;
|
||||
use valence::packet::s2c::play::TitleFadeS2c;
|
||||
use valence::prelude::*;
|
||||
use valence::protocol::packet::s2c::play::TitleFadeS2c;
|
||||
use valence::protocol::sound::Sound;
|
||||
use valence::protocol::types::SoundCategory;
|
||||
use valence::sound::{Sound, SoundCategory};
|
||||
|
||||
const START_POS: BlockPos = BlockPos::new(0, 100, 0);
|
||||
const VIEW_DIST: u8 = 10;
|
||||
|
@ -27,7 +26,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_system(init_clients)
|
||||
.add_systems((
|
||||
reset_clients.after(init_clients),
|
||||
|
|
|
@ -10,7 +10,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_system(despawn_disconnected_clients)
|
||||
|
@ -91,8 +91,8 @@ fn create_particle_vec() -> Vec<Particle> {
|
|||
vec![
|
||||
Particle::AmbientEntityEffect,
|
||||
Particle::AngryVillager,
|
||||
Particle::Block(BlockState::OAK_PLANKS),
|
||||
Particle::BlockMarker(BlockState::GOLD_BLOCK),
|
||||
Particle::Block(BlockState::OAK_PLANKS.to_raw() as _),
|
||||
Particle::BlockMarker(BlockState::GOLD_BLOCK.to_raw() as _),
|
||||
Particle::Bubble,
|
||||
Particle::Cloud,
|
||||
Particle::Crit,
|
||||
|
@ -104,13 +104,13 @@ fn create_particle_vec() -> Vec<Particle> {
|
|||
Particle::DrippingWater,
|
||||
Particle::FallingWater,
|
||||
Particle::Dust {
|
||||
rgb: [1.0, 1.0, 0.0],
|
||||
rgb: Vec3::new(1.0, 1.0, 0.0),
|
||||
scale: 2.0,
|
||||
},
|
||||
Particle::DustColorTransition {
|
||||
from_rgb: [1.0, 0.0, 0.0],
|
||||
from_rgb: Vec3::new(1.0, 0.0, 0.0),
|
||||
scale: 2.0,
|
||||
to_rgb: [0.0, 1.0, 0.0],
|
||||
to_rgb: Vec3::new(0.0, 1.0, 0.0),
|
||||
},
|
||||
Particle::Effect,
|
||||
Particle::ElderGuardian,
|
||||
|
@ -121,7 +121,7 @@ fn create_particle_vec() -> Vec<Particle> {
|
|||
Particle::ExplosionEmitter,
|
||||
Particle::Explosion,
|
||||
Particle::SonicBoom,
|
||||
Particle::FallingDust(BlockState::RED_SAND),
|
||||
Particle::FallingDust(BlockState::RED_SAND.to_raw() as _),
|
||||
Particle::Firework,
|
||||
Particle::Fishing,
|
||||
Particle::Flame,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use rand::Rng;
|
||||
use valence::player_list::{DisplayName, PlayerListEntryBundle};
|
||||
use valence::prelude::*;
|
||||
use valence_client::Ping;
|
||||
|
||||
const SPAWN_Y: i32 = 64;
|
||||
const PLAYER_UUID_1: Uuid = Uuid::from_u128(1);
|
||||
|
@ -12,7 +13,7 @@ fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_systems((
|
||||
init_clients,
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
use valence::client::misc::{ResourcePackStatus, ResourcePackStatusChange};
|
||||
use valence::entity::player::PlayerEntityBundle;
|
||||
use valence::entity::sheep::SheepEntityBundle;
|
||||
use valence::packet::c2s::play::player_interact_entity::EntityInteraction;
|
||||
use valence::prelude::*;
|
||||
use valence::protocol::packet::c2s::play::player_interact_entity::EntityInteraction;
|
||||
|
||||
const SPAWN_Y: i32 = 64;
|
||||
|
||||
|
@ -12,7 +12,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_systems((init_clients, prompt_on_punch, on_resource_pack_status))
|
||||
.add_system(despawn_disconnected_clients)
|
||||
|
|
|
@ -2,27 +2,38 @@
|
|||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use rand::Rng;
|
||||
use valence::prelude::*;
|
||||
use valence_network::{async_trait, CleanupFn, ConnectionMode, PlayerSampleEntry, ServerListPing};
|
||||
|
||||
pub fn main() {
|
||||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(MyCallbacks).with_connection_mode(ConnectionMode::Offline))
|
||||
.insert_resource(NetworkSettings {
|
||||
connection_mode: ConnectionMode::Offline,
|
||||
callbacks: MyCallbacks.into(),
|
||||
..Default::default()
|
||||
})
|
||||
.add_plugins(DefaultPlugins)
|
||||
.run();
|
||||
}
|
||||
|
||||
struct MyCallbacks;
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncCallbacks for MyCallbacks {
|
||||
impl NetworkCallbacks for MyCallbacks {
|
||||
async fn server_list_ping(
|
||||
&self,
|
||||
_shared: &SharedServer,
|
||||
_shared: &SharedNetworkState,
|
||||
remote_addr: SocketAddr,
|
||||
_protocol_version: i32,
|
||||
) -> ServerListPing {
|
||||
let max_players = 420;
|
||||
|
||||
ServerListPing::Respond {
|
||||
online_players: 42,
|
||||
max_players: 420,
|
||||
online_players: rand::thread_rng().gen_range(0..=max_players),
|
||||
max_players,
|
||||
player_sample: vec![PlayerSampleEntry {
|
||||
name: "foobar".into(),
|
||||
id: Uuid::from_u128(12345),
|
||||
|
@ -33,7 +44,11 @@ impl AsyncCallbacks for MyCallbacks {
|
|||
}
|
||||
}
|
||||
|
||||
async fn login(&self, _shared: &SharedServer, _info: &NewClientInfo) -> Result<(), Text> {
|
||||
async fn login(
|
||||
&self,
|
||||
_shared: &SharedNetworkState,
|
||||
_info: &NewClientInfo,
|
||||
) -> Result<CleanupFn, Text> {
|
||||
Err("You are not meant to join this example".color(Color::RED))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_systems(
|
||||
(
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use valence::prelude::*;
|
||||
use valence::protocol::translation_key;
|
||||
|
||||
const SPAWN_Y: i32 = 64;
|
||||
|
||||
|
@ -9,7 +8,7 @@ pub fn main() {
|
|||
tracing_subscriber::fmt().init();
|
||||
|
||||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_system(despawn_disconnected_clients)
|
||||
|
|
|
@ -1,276 +0,0 @@
|
|||
use std::fmt;
|
||||
|
||||
use bevy_app::{CoreSet, Plugin};
|
||||
/// Contains shared components and world queries.
|
||||
use bevy_ecs::prelude::*;
|
||||
use glam::{DVec3, Vec3};
|
||||
use uuid::Uuid;
|
||||
use valence_protocol::types::{GameMode as ProtocolGameMode, Property};
|
||||
|
||||
use crate::client::FlushPacketsSet;
|
||||
use crate::util::{from_yaw_and_pitch, to_yaw_and_pitch};
|
||||
use crate::view::ChunkPos;
|
||||
|
||||
pub(crate) struct ComponentPlugin;
|
||||
|
||||
impl Plugin for ComponentPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.add_systems(
|
||||
(update_old_position, update_old_location)
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.after(FlushPacketsSet),
|
||||
)
|
||||
.add_system(despawn_marked_entities.in_base_set(CoreSet::Last));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_old_position(mut query: Query<(&Position, &mut OldPosition)>) {
|
||||
for (pos, mut old_pos) in &mut query {
|
||||
old_pos.0 = pos.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_old_location(mut query: Query<(&Location, &mut OldLocation)>) {
|
||||
for (loc, mut old_loc) in &mut query {
|
||||
old_loc.0 = loc.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Component`] for marking entities that should be despawned at the end of
|
||||
/// the tick.
|
||||
///
|
||||
/// In Valence, some entities such as [Minecraft entities] 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.
|
||||
///
|
||||
/// [Minecraft entities]: crate::entity
|
||||
#[derive(Component, Copy, Clone, Default, PartialEq, Eq, Debug)]
|
||||
pub struct Despawned;
|
||||
|
||||
/// The universally unique identifier of an entity. Component wrapper for a
|
||||
/// [`Uuid`].
|
||||
///
|
||||
/// This component is expected to remain _unique_ and _constant_ during the
|
||||
/// lifetime of the entity. The [`Default`] impl generates a new random UUID.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct UniqueId(pub Uuid);
|
||||
|
||||
/// Generates a new random UUID.
|
||||
impl Default for UniqueId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::from_bytes(rand::random()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, PartialEq, Eq, Default, Debug)]
|
||||
pub struct Username(pub String);
|
||||
|
||||
impl fmt::Display for Username {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, PartialEq, Eq, Default, Debug)]
|
||||
pub struct Properties(pub Vec<Property>);
|
||||
|
||||
impl Properties {
|
||||
/// Finds the property with the name "textures".
|
||||
pub fn textures(&self) -> Option<&Property> {
|
||||
self.0.iter().find(|prop| prop.name == "textures")
|
||||
}
|
||||
|
||||
/// Finds the property with the name "textures".
|
||||
pub fn textures_mut(&mut self) -> Option<&mut Property> {
|
||||
self.0.iter_mut().find(|prop| prop.name == "textures")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
|
||||
|
||||
pub enum GameMode {
|
||||
#[default]
|
||||
Survival,
|
||||
Creative,
|
||||
Adventure,
|
||||
Spectator,
|
||||
}
|
||||
|
||||
impl From<GameMode> for ProtocolGameMode {
|
||||
fn from(gm: GameMode) -> Self {
|
||||
match gm {
|
||||
GameMode::Survival => ProtocolGameMode::Survival,
|
||||
GameMode::Creative => ProtocolGameMode::Creative,
|
||||
GameMode::Adventure => ProtocolGameMode::Adventure,
|
||||
GameMode::Spectator => ProtocolGameMode::Spectator,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProtocolGameMode> for GameMode {
|
||||
fn from(gm: ProtocolGameMode) -> Self {
|
||||
match gm {
|
||||
ProtocolGameMode::Survival => GameMode::Survival,
|
||||
ProtocolGameMode::Creative => GameMode::Creative,
|
||||
ProtocolGameMode::Adventure => GameMode::Adventure,
|
||||
ProtocolGameMode::Spectator => GameMode::Spectator,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delay measured in milliseconds. Negative values indicate absence.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct Ping(pub i32);
|
||||
|
||||
impl Default for Ping {
|
||||
fn default() -> Self {
|
||||
Self(-1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the [`Instance`] an entity is located in. For the coordinates
|
||||
/// within the instance, see [`Position`].
|
||||
///
|
||||
/// [`Instance`]: crate::instance::Instance
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Location(pub Entity);
|
||||
|
||||
impl Default for Location {
|
||||
fn default() -> Self {
|
||||
Self(Entity::PLACEHOLDER)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<OldLocation> for Location {
|
||||
fn eq(&self, other: &OldLocation) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of [`Location`] from the end of the previous tick.
|
||||
///
|
||||
/// **NOTE**: You should not modify this component after the entity is spawned.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct OldLocation(Entity);
|
||||
|
||||
impl OldLocation {
|
||||
pub fn new(instance: Entity) -> Self {
|
||||
Self(instance)
|
||||
}
|
||||
|
||||
pub fn get(&self) -> Entity {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OldLocation {
|
||||
fn default() -> Self {
|
||||
Self(Entity::PLACEHOLDER)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Location> for OldLocation {
|
||||
fn eq(&self, other: &Location) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Copy, Clone, PartialEq, Default, Debug)]
|
||||
pub struct Position(pub DVec3);
|
||||
|
||||
impl Position {
|
||||
pub fn new(pos: impl Into<DVec3>) -> Self {
|
||||
Self(pos.into())
|
||||
}
|
||||
|
||||
pub fn chunk_pos(&self) -> ChunkPos {
|
||||
ChunkPos::from_dvec3(self.0)
|
||||
}
|
||||
|
||||
pub fn get(self) -> DVec3 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn set(&mut self, pos: impl Into<DVec3>) {
|
||||
self.0 = pos.into();
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<OldPosition> for Position {
|
||||
fn eq(&self, other: &OldPosition) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of [`Location`] from the end of the previous tick.
|
||||
///
|
||||
/// **NOTE**: You should not modify this component after the entity is spawned.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Default, Debug)]
|
||||
pub struct OldPosition(DVec3);
|
||||
|
||||
impl OldPosition {
|
||||
pub fn new(pos: impl Into<DVec3>) -> Self {
|
||||
Self(pos.into())
|
||||
}
|
||||
|
||||
pub fn get(self) -> DVec3 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn chunk_pos(self) -> ChunkPos {
|
||||
ChunkPos::from_dvec3(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Position> for OldPosition {
|
||||
fn eq(&self, other: &Position) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the direction an entity is looking using pitch and yaw angles.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Default, Debug)]
|
||||
pub struct Look {
|
||||
/// The yaw angle in degrees.
|
||||
pub yaw: f32,
|
||||
/// The pitch angle in degrees.
|
||||
pub pitch: f32,
|
||||
}
|
||||
|
||||
impl Look {
|
||||
pub const fn new(yaw: f32, pitch: f32) -> Self {
|
||||
Self { yaw, pitch }
|
||||
}
|
||||
|
||||
/// Gets a normalized direction vector from the yaw and pitch.
|
||||
pub fn vec(&self) -> Vec3 {
|
||||
from_yaw_and_pitch(self.yaw, self.pitch)
|
||||
}
|
||||
|
||||
/// Sets the yaw and pitch using a normalized direction vector.
|
||||
pub fn set_vec(&mut self, vec: Vec3) {
|
||||
(self.yaw, self.pitch) = to_yaw_and_pitch(vec);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
|
||||
pub struct OnGround(pub bool);
|
||||
|
||||
/// General-purpose reusable byte buffer.
|
||||
///
|
||||
/// No guarantees are made about the buffer's contents between systems.
|
||||
/// Therefore, the inner `Vec` should be cleared before use.
|
||||
#[derive(Component, Default, Debug)]
|
||||
pub struct ScratchBuf(pub Vec<u8>);
|
|
@ -1,388 +0,0 @@
|
|||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bevy_app::{App, Plugin};
|
||||
use serde::Serialize;
|
||||
use tokio::runtime::Handle;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
use valence_protocol::text::Text;
|
||||
|
||||
use crate::server::{NewClientInfo, SharedServer};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct ServerPlugin<A> {
|
||||
pub callbacks: Arc<A>,
|
||||
/// The [`Handle`] to the tokio runtime the server will use. If `None` is
|
||||
/// provided, the server will create its own tokio runtime at startup.
|
||||
///
|
||||
/// # Default Value
|
||||
///
|
||||
/// `None`
|
||||
pub tokio_handle: Option<Handle>,
|
||||
/// The maximum number of simultaneous connections allowed to the server.
|
||||
/// This includes all connections, not just those past the login stage.
|
||||
///
|
||||
/// You will want this value to be somewhere above the maximum number of
|
||||
/// players, since status pings should still succeed even when the server is
|
||||
/// full.
|
||||
///
|
||||
/// # Default Value
|
||||
///
|
||||
/// `1024`. This may change in a future version.
|
||||
pub max_connections: usize,
|
||||
/// The socket address the server will be bound to.
|
||||
///
|
||||
/// # Default Value
|
||||
///
|
||||
/// `0.0.0.0:25565`, which will listen on every available network interface.
|
||||
pub address: SocketAddr,
|
||||
/// The ticks per second of the server. This is the number of game updates
|
||||
/// that should occur in one second.
|
||||
///
|
||||
/// On each game update (tick), the server is expected to update game logic
|
||||
/// and respond to packets from clients. Once this is complete, the server
|
||||
/// will sleep for any remaining time until a full tick has passed.
|
||||
///
|
||||
/// The tick rate must be greater than zero.
|
||||
///
|
||||
/// Note that the official Minecraft client only processes packets at 20hz,
|
||||
/// so there is little benefit to a tick rate higher than 20.
|
||||
///
|
||||
/// # Default Value
|
||||
///
|
||||
/// [`DEFAULT_TPS`]
|
||||
pub tps: i64,
|
||||
/// The connection mode. This determines if client authentication and
|
||||
/// encryption should take place and if the server should get the player
|
||||
/// data from a proxy.
|
||||
///
|
||||
/// # Default Value
|
||||
///
|
||||
/// [`ConnectionMode::Online`]
|
||||
pub connection_mode: ConnectionMode,
|
||||
/// The compression threshold to use for compressing packets. For a
|
||||
/// compression threshold of `Some(N)`, packets with encoded lengths >= `N`
|
||||
/// are compressed while all others are not. `None` disables compression
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
///
|
||||
/// A default placeholder response is returned.
|
||||
async fn server_list_ping(
|
||||
&self,
|
||||
shared: &SharedServer,
|
||||
remote_addr: SocketAddr,
|
||||
protocol_version: i32,
|
||||
) -> ServerListPing {
|
||||
#![allow(unused_variables)]
|
||||
ServerListPing::Respond {
|
||||
online_players: 0, // TODO: get online players.
|
||||
max_players: -1,
|
||||
player_sample: vec![],
|
||||
description: "A Valence Server".into(),
|
||||
favicon_png: &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// Called for each client after successful authentication (if online mode
|
||||
/// is enabled) to determine if they can join the server. On success, a
|
||||
/// new entity is spawned with the [`Client`] component. If this method
|
||||
/// returns with `Err(reason)`, then the client is immediately
|
||||
/// disconnected with `reason` as the displayed message.
|
||||
///
|
||||
/// This method is the appropriate place to perform asynchronous
|
||||
/// operations such as database queries which may take some time to
|
||||
/// complete.
|
||||
///
|
||||
/// This method is called from within a tokio runtime.
|
||||
///
|
||||
/// # Default Implementation
|
||||
///
|
||||
/// The client is allowed to join unconditionally.
|
||||
///
|
||||
/// [`Client`]: crate::client::Client
|
||||
async fn login(&self, shared: &SharedServer, info: &NewClientInfo) -> Result<(), Text> {
|
||||
#![allow(unused_variables)]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called upon every client login to obtain the full URL to use for session
|
||||
/// server requests. This is done to authenticate player accounts. This
|
||||
/// method is not called unless [online mode] is enabled.
|
||||
///
|
||||
/// It is assumed that upon successful request, a structure matching the
|
||||
/// description in the [wiki](https://wiki.vg/Protocol_Encryption#Server) was obtained.
|
||||
/// Providing a URL that does not return such a structure will result in a
|
||||
/// disconnect for every client that connects.
|
||||
///
|
||||
/// The arguments are described in the linked wiki article.
|
||||
///
|
||||
/// # Default Implementation
|
||||
///
|
||||
/// Uses the official Minecraft session server. This is formatted as
|
||||
/// `https://sessionserver.mojang.com/session/minecraft/hasJoined?username=<username>&serverId=<auth-digest>&ip=<player-ip>`.
|
||||
///
|
||||
/// [online mode]: crate::config::ConnectionMode::Online
|
||||
async fn session_server(
|
||||
&self,
|
||||
shared: &SharedServer,
|
||||
username: &str,
|
||||
auth_digest: &str,
|
||||
player_ip: &IpAddr,
|
||||
) -> String {
|
||||
if shared.connection_mode()
|
||||
== (&ConnectionMode::Online {
|
||||
prevent_proxy_connections: true,
|
||||
})
|
||||
{
|
||||
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={auth_digest}&ip={player_ip}")
|
||||
} else {
|
||||
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={auth_digest}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The default async callbacks.
|
||||
impl AsyncCallbacks for () {}
|
||||
|
||||
/// Describes how new connections to the server are handled.
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ConnectionMode {
|
||||
/// The "online mode" fetches all player data (username, UUID, and skin)
|
||||
/// from the [configured session server] and enables encryption.
|
||||
///
|
||||
/// This mode should be used by all publicly exposed servers which are not
|
||||
/// behind a proxy.
|
||||
///
|
||||
/// [configured session server]: AsyncCallbacks::session_server
|
||||
Online {
|
||||
/// Determines if client IP validation should take place during
|
||||
/// authentication.
|
||||
///
|
||||
/// When `prevent_proxy_connections` is enabled, clients can no longer
|
||||
/// log-in if they connected to the Yggdrasil server using a different
|
||||
/// IP than the one used to connect to this server.
|
||||
///
|
||||
/// This is used by the default implementation of
|
||||
/// [`AsyncCallbacks::session_server`]. A different implementation may
|
||||
/// choose to ignore this value.
|
||||
prevent_proxy_connections: bool,
|
||||
},
|
||||
/// Disables client authentication with the configured session server.
|
||||
/// Clients can join with any username and UUID they choose, potentially
|
||||
/// gaining privileges they would not otherwise have. Additionally,
|
||||
/// encryption is disabled and Minecraft's default skins will be used.
|
||||
///
|
||||
/// This mode should be used for development purposes only and not for
|
||||
/// publicly exposed servers.
|
||||
Offline,
|
||||
/// This mode should be used under one of the following situations:
|
||||
/// - The server is behind a [BungeeCord]/[Waterfall] proxy with IP
|
||||
/// forwarding enabled.
|
||||
/// - The server is behind a [Velocity] proxy configured to use the `legacy`
|
||||
/// forwarding mode.
|
||||
///
|
||||
/// All player data (username, UUID, and skin) is fetched from the proxy,
|
||||
/// but no attempt is made to stop connections originating from
|
||||
/// elsewhere. As a result, you must ensure clients connect through the
|
||||
/// proxy and are unable to connect to the server directly. Otherwise,
|
||||
/// clients can use any username or UUID they choose similar to
|
||||
/// [`ConnectionMode::Offline`].
|
||||
///
|
||||
/// To protect against this, a firewall can be used. However,
|
||||
/// [`ConnectionMode::Velocity`] is recommended as a secure alternative.
|
||||
///
|
||||
/// [BungeeCord]: https://www.spigotmc.org/wiki/bungeecord/
|
||||
/// [Waterfall]: https://github.com/PaperMC/Waterfall
|
||||
/// [Velocity]: https://velocitypowered.com/
|
||||
BungeeCord,
|
||||
/// This mode is used when the server is behind a [Velocity] proxy
|
||||
/// configured with the forwarding mode `modern`.
|
||||
///
|
||||
/// All player data (username, UUID, and skin) is fetched from the proxy and
|
||||
/// all connections originating from outside Velocity are blocked.
|
||||
///
|
||||
/// [Velocity]: https://velocitypowered.com/
|
||||
Velocity {
|
||||
/// The secret key used to prevent connections from outside Velocity.
|
||||
/// The proxy and Valence must be configured to use the same secret key.
|
||||
secret: Arc<str>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Minecraft's standard ticks per second (TPS).
|
||||
pub const DEFAULT_TPS: i64 = 20;
|
||||
|
||||
/// The result of the Server List Ping [callback].
|
||||
///
|
||||
/// [callback]: crate::config::AsyncCallbacks
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub enum ServerListPing<'a> {
|
||||
/// Responds to the server list ping with the given information.
|
||||
Respond {
|
||||
/// Displayed as the number of players on the server.
|
||||
online_players: i32,
|
||||
/// Displayed as the maximum number of players allowed on the server at
|
||||
/// a time.
|
||||
max_players: i32,
|
||||
/// The list of players visible by hovering over the player count.
|
||||
///
|
||||
/// Has no effect if this list is empty.
|
||||
player_sample: Vec<PlayerSampleEntry>,
|
||||
/// A description of the server.
|
||||
description: Text,
|
||||
/// The server's icon as the bytes of a PNG image.
|
||||
/// The image must be 64x64 pixels.
|
||||
///
|
||||
/// No icon is used if the slice is empty.
|
||||
favicon_png: &'a [u8],
|
||||
},
|
||||
/// Ignores the query and disconnects from the client.
|
||||
#[default]
|
||||
Ignore,
|
||||
}
|
||||
|
||||
/// Represents an individual entry in the player sample.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct PlayerSampleEntry {
|
||||
/// The name of the player.
|
||||
///
|
||||
/// This string can contain
|
||||
/// [legacy formatting codes](https://minecraft.fandom.com/wiki/Formatting_codes).
|
||||
pub name: String,
|
||||
/// The player UUID.
|
||||
pub id: Uuid,
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
//! Valence is a Minecraft server framework written in Rust.
|
||||
|
||||
#![doc = include_str!("../../../README.md")] // Points to the main project README.
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/valence-rs/valence/main/assets/logo.svg",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/valence-rs/valence/main/assets/logo.svg"
|
||||
|
@ -18,72 +17,133 @@
|
|||
trivial_numeric_casts,
|
||||
unused_lifetimes,
|
||||
unused_import_braces,
|
||||
unreachable_pub,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
#![allow(clippy::type_complexity)] // ECS queries are often complicated.
|
||||
|
||||
use bevy_app::{PluginGroup, PluginGroupBuilder};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "anvil")]
|
||||
pub use valence_anvil as anvil;
|
||||
pub use valence_core::*;
|
||||
#[cfg(feature = "inventory")]
|
||||
pub use valence_inventory as inventory;
|
||||
#[cfg(feature = "network")]
|
||||
pub use valence_network as network;
|
||||
#[cfg(feature = "player_list")]
|
||||
pub use valence_player_list as player_list;
|
||||
pub use {
|
||||
anyhow, async_trait, bevy_app, bevy_ecs, uuid, valence_nbt as nbt, valence_protocol as protocol,
|
||||
bevy_app as app, bevy_ecs as ecs, glam, valence_biome as biome, valence_block as block,
|
||||
valence_client as client, valence_dimension as dimension, valence_entity as entity,
|
||||
valence_instance as instance, valence_nbt as nbt, valence_registry as registry,
|
||||
};
|
||||
|
||||
pub mod biome;
|
||||
pub mod client;
|
||||
pub mod component;
|
||||
pub mod config;
|
||||
pub mod dimension;
|
||||
pub mod entity;
|
||||
pub mod event_loop;
|
||||
pub mod instance;
|
||||
pub mod inventory;
|
||||
pub mod packet;
|
||||
pub mod player_list;
|
||||
pub mod player_textures;
|
||||
pub mod registry_codec;
|
||||
pub mod server;
|
||||
#[cfg(any(test, doctest))]
|
||||
mod unit_test;
|
||||
pub mod util;
|
||||
pub mod view;
|
||||
pub mod weather;
|
||||
|
||||
/// Contains the most frequently used items in Valence projects.
|
||||
///
|
||||
/// This is usually glob imported like so:
|
||||
///
|
||||
/// ```
|
||||
/// use valence::prelude::*; // Glob import.
|
||||
///
|
||||
/// let mut app = App::new();
|
||||
/// app.add_system(|| println!("yippee!"));
|
||||
/// // ...
|
||||
/// ```
|
||||
pub mod prelude {
|
||||
pub use async_trait::async_trait;
|
||||
pub use bevy_app::prelude::*;
|
||||
pub use bevy_ecs::prelude::*;
|
||||
pub use ::uuid::Uuid;
|
||||
pub use app::prelude::*;
|
||||
pub use biome::{Biome, BiomeId, BiomeRegistry};
|
||||
pub use block::{BlockKind, BlockState, PropName, PropValue};
|
||||
pub use block_pos::BlockPos;
|
||||
pub use chunk_pos::{ChunkPos, ChunkView};
|
||||
pub use client::action::*;
|
||||
pub use client::command::*;
|
||||
pub use client::event_loop::{EventLoopSchedule, EventLoopSet};
|
||||
pub use client::interact_entity::*;
|
||||
pub use client::{
|
||||
despawn_disconnected_clients, Client, CompassPos, CursorItem, DeathLocation,
|
||||
HasRespawnScreen, HashedSeed, Ip, IsDebug, IsFlat, IsHardcore, OldView, OldViewDistance,
|
||||
OpLevel, PrevGameMode, ReducedDebugInfo, View, ViewDistance,
|
||||
};
|
||||
pub use component::*;
|
||||
pub use config::{
|
||||
AsyncCallbacks, ConnectionMode, PlayerSampleEntry, ServerListPing, ServerPlugin,
|
||||
despawn_disconnected_clients, Client, CompassPos, DeathLocation, HasRespawnScreen,
|
||||
HashedSeed, Ip, IsDebug, IsFlat, IsHardcore, OldView, OldViewDistance, OpLevel,
|
||||
PrevGameMode, Properties, ReducedDebugInfo, Username, View, ViewDistance,
|
||||
};
|
||||
pub use despawn::Despawned;
|
||||
pub use dimension::{DimensionType, DimensionTypeRegistry};
|
||||
pub use entity::{EntityAnimation, EntityKind, EntityManager, EntityStatus, HeadYaw};
|
||||
pub use event_loop::{EventLoopSchedule, EventLoopSet};
|
||||
pub use glam::DVec3;
|
||||
pub use instance::{Block, BlockMut, BlockRef, Chunk, Instance};
|
||||
pub use inventory::{
|
||||
Inventory, InventoryKind, InventoryWindow, InventoryWindowMut, OpenInventory,
|
||||
pub use direction::Direction;
|
||||
pub use ecs::prelude::*;
|
||||
pub use entity::{
|
||||
EntityAnimation, EntityKind, EntityManager, EntityStatus, HeadYaw, Location, Look,
|
||||
OldLocation, OldPosition, Position,
|
||||
};
|
||||
pub use game_mode::GameMode;
|
||||
pub use glam::{DVec2, DVec3, Vec2, Vec3};
|
||||
pub use hand::Hand;
|
||||
pub use ident::Ident;
|
||||
pub use instance::{Block, BlockMut, BlockRef, Chunk, Instance};
|
||||
#[cfg(feature = "inventory")]
|
||||
pub use inventory::{
|
||||
CursorItem, Inventory, InventoryKind, InventoryWindow, InventoryWindowMut, OpenInventory,
|
||||
};
|
||||
pub use item::{ItemKind, ItemStack};
|
||||
pub use nbt::Compound;
|
||||
#[cfg(feature = "network")]
|
||||
pub use network::{
|
||||
ErasedNetworkCallbacks, NetworkCallbacks, NetworkSettings, NewClientInfo,
|
||||
SharedNetworkState,
|
||||
};
|
||||
pub use packet::s2c::play::particle::Particle;
|
||||
#[cfg(feature = "player_list")]
|
||||
pub use player_list::{PlayerList, PlayerListEntry};
|
||||
pub use protocol::block::{BlockState, PropName, PropValue};
|
||||
pub use protocol::ident::Ident;
|
||||
pub use protocol::item::{ItemKind, ItemStack};
|
||||
pub use protocol::text::{Color, Text, TextFormat};
|
||||
pub use server::{NewClientInfo, Server, SharedServer};
|
||||
pub use uuid::Uuid;
|
||||
pub use valence_nbt::Compound;
|
||||
pub use valence_protocol::block::BlockKind;
|
||||
pub use valence_protocol::block_pos::BlockPos;
|
||||
pub use valence_protocol::ident;
|
||||
pub use valence_protocol::packet::s2c::play::particle::Particle;
|
||||
pub use view::{ChunkPos, ChunkView};
|
||||
pub use text::{Color, Text, TextFormat};
|
||||
pub use valence_core::ident; // Export the `ident!` macro.
|
||||
pub use valence_core::uuid::UniqueId;
|
||||
pub use valence_core::{translation_key, CoreSettings, Server};
|
||||
|
||||
pub use super::DefaultPlugins;
|
||||
use super::*;
|
||||
}
|
||||
|
||||
/// This plugin group will add all the default plugins for a Valence
|
||||
/// application.
|
||||
///
|
||||
/// [`DefaultPlugins`] obeys Cargo feature flags. Users may exert control over
|
||||
/// this plugin group by disabling `default-features` in their `Cargo.toml` and
|
||||
/// enabling only those features that they wish to use.
|
||||
pub struct DefaultPlugins;
|
||||
|
||||
impl PluginGroup for DefaultPlugins {
|
||||
fn build(self) -> PluginGroupBuilder {
|
||||
#[allow(unused_mut)]
|
||||
let mut group = PluginGroupBuilder::start::<Self>()
|
||||
.add(valence_core::CorePlugin)
|
||||
.add(valence_registry::RegistryPlugin)
|
||||
.add(valence_biome::BiomePlugin)
|
||||
.add(valence_dimension::DimensionPlugin)
|
||||
.add(valence_entity::EntityPlugin)
|
||||
.add(valence_instance::InstancePlugin)
|
||||
.add(valence_client::ClientPlugin);
|
||||
|
||||
#[cfg(feature = "network")]
|
||||
{
|
||||
group = group.add(valence_network::NetworkPlugin);
|
||||
}
|
||||
|
||||
#[cfg(feature = "player_list")]
|
||||
{
|
||||
group = group.add(valence_player_list::PlayerListPlugin);
|
||||
}
|
||||
|
||||
#[cfg(feature = "inventory")]
|
||||
{
|
||||
group = group.add(valence_inventory::InventoryPlugin);
|
||||
}
|
||||
|
||||
#[cfg(feature = "anvil")]
|
||||
{
|
||||
// No plugin... yet.
|
||||
}
|
||||
|
||||
group
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
use std::io::Write;
|
||||
|
||||
use tracing::warn;
|
||||
use valence_protocol::encoder::{encode_packet, encode_packet_compressed, PacketEncoder};
|
||||
use valence_protocol::Packet;
|
||||
|
||||
/// Types that can have packets written to them.
|
||||
pub trait WritePacket {
|
||||
/// Writes a packet to this object. Encoding errors are typically logged and
|
||||
/// discarded.
|
||||
fn write_packet<'a>(&mut self, packet: &impl Packet<'a>);
|
||||
/// Copies raw packet data directly into this object. Don't use this unless
|
||||
/// you know what you're doing.
|
||||
fn write_packet_bytes(&mut self, bytes: &[u8]);
|
||||
}
|
||||
|
||||
impl<W: WritePacket> WritePacket for &mut W {
|
||||
fn write_packet<'a>(&mut self, packet: &impl Packet<'a>) {
|
||||
(*self).write_packet(packet)
|
||||
}
|
||||
|
||||
fn write_packet_bytes(&mut self, bytes: &[u8]) {
|
||||
(*self).write_packet_bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// An implementor of [`WritePacket`] backed by a `Vec` reference.
|
||||
pub(crate) struct PacketWriter<'a> {
|
||||
buf: &'a mut Vec<u8>,
|
||||
threshold: Option<u32>,
|
||||
scratch: &'a mut Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a> PacketWriter<'a> {
|
||||
pub fn new(buf: &'a mut Vec<u8>, threshold: Option<u32>, scratch: &'a mut Vec<u8>) -> Self {
|
||||
Self {
|
||||
buf,
|
||||
threshold,
|
||||
scratch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WritePacket for PacketWriter<'_> {
|
||||
fn write_packet<'a>(&mut self, pkt: &impl Packet<'a>) {
|
||||
let res = if let Some(threshold) = self.threshold {
|
||||
encode_packet_compressed(self.buf, pkt, threshold, self.scratch)
|
||||
} else {
|
||||
encode_packet(self.buf, pkt)
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
warn!("failed to write packet: {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
fn write_packet_bytes(&mut self, bytes: &[u8]) {
|
||||
if let Err(e) = self.buf.write_all(bytes) {
|
||||
warn!("failed to write packet bytes: {e:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WritePacket for PacketEncoder {
|
||||
fn write_packet<'a>(&mut self, packet: &impl Packet<'a>) {
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -1,288 +0,0 @@
|
|||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::ensure;
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_app::{ScheduleRunnerPlugin, ScheduleRunnerSettings};
|
||||
use bevy_ecs::prelude::*;
|
||||
use flume::{Receiver, Sender};
|
||||
use rand::rngs::OsRng;
|
||||
use rsa::{PublicKeyParts, RsaPrivateKey};
|
||||
use tokio::runtime::{Handle, Runtime};
|
||||
use tokio::sync::Semaphore;
|
||||
use uuid::Uuid;
|
||||
use valence_protocol::types::Property;
|
||||
|
||||
use crate::biome::BiomePlugin;
|
||||
use crate::client::{ClientBundle, ClientPlugin};
|
||||
use crate::component::ComponentPlugin;
|
||||
use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin};
|
||||
use crate::dimension::DimensionPlugin;
|
||||
use crate::entity::EntityPlugin;
|
||||
use crate::event_loop::{EventLoopPlugin, RunEventLoopSet};
|
||||
use crate::instance::InstancePlugin;
|
||||
use crate::inventory::InventoryPlugin;
|
||||
use crate::player_list::PlayerListPlugin;
|
||||
use crate::registry_codec::RegistryCodecPlugin;
|
||||
use crate::server::connect::do_accept_loop;
|
||||
use crate::weather::WeatherPlugin;
|
||||
|
||||
mod byte_channel;
|
||||
mod connect;
|
||||
pub(crate) mod connection;
|
||||
|
||||
use connection::NewClientArgs;
|
||||
|
||||
/// Contains global server state accessible as a [`Resource`].
|
||||
#[derive(Resource)]
|
||||
pub struct Server {
|
||||
/// Incremented on every tick.
|
||||
current_tick: i64,
|
||||
shared: SharedServer,
|
||||
}
|
||||
|
||||
impl Deref for Server {
|
||||
type Target = SharedServer;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.shared
|
||||
}
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Provides a reference to the [`SharedServer`].
|
||||
pub fn shared(&self) -> &SharedServer {
|
||||
&self.shared
|
||||
}
|
||||
|
||||
/// Returns the number of ticks that have elapsed since the server began.
|
||||
pub fn current_tick(&self) -> i64 {
|
||||
self.current_tick
|
||||
}
|
||||
}
|
||||
|
||||
/// The subset of global server state which can be shared between threads.
|
||||
///
|
||||
/// `SharedServer`s are internally refcounted and are inexpensive to clone.
|
||||
#[derive(Clone)]
|
||||
pub struct SharedServer(Arc<SharedServerInner>);
|
||||
|
||||
struct SharedServerInner {
|
||||
address: SocketAddr,
|
||||
tps: i64,
|
||||
connection_mode: ConnectionMode,
|
||||
compression_threshold: Option<u32>,
|
||||
max_connections: usize,
|
||||
incoming_capacity: usize,
|
||||
outgoing_capacity: usize,
|
||||
/// The tokio handle used by the server.
|
||||
tokio_handle: Handle,
|
||||
/// Holding a runtime handle is not enough to keep tokio working. We need
|
||||
/// to store the runtime here so we don't drop it.
|
||||
_tokio_runtime: Option<Runtime>,
|
||||
/// Sender for new clients past the login stage.
|
||||
new_clients_send: Sender<NewClientArgs>,
|
||||
/// Receiver for new clients past the login stage.
|
||||
new_clients_recv: Receiver<NewClientArgs>,
|
||||
/// A semaphore used to limit the number of simultaneous connections to the
|
||||
/// server. Closing this semaphore stops new connections.
|
||||
connection_sema: Arc<Semaphore>,
|
||||
/// The RSA keypair used for encryption with clients.
|
||||
rsa_key: RsaPrivateKey,
|
||||
/// The public part of `rsa_key` encoded in DER, which is an ASN.1 format.
|
||||
/// This is sent to clients during the authentication process.
|
||||
public_key_der: Box<[u8]>,
|
||||
/// For session server requests.
|
||||
http_client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl SharedServer {
|
||||
/// Gets the socket address this server is bound to.
|
||||
pub fn address(&self) -> SocketAddr {
|
||||
self.0.address
|
||||
}
|
||||
|
||||
/// Gets the configured ticks per second of this server.
|
||||
pub fn tps(&self) -> i64 {
|
||||
self.0.tps
|
||||
}
|
||||
|
||||
/// Gets the connection mode of the server.
|
||||
pub fn connection_mode(&self) -> &ConnectionMode {
|
||||
&self.0.connection_mode
|
||||
}
|
||||
|
||||
/// Gets the compression threshold for packets. `None` indicates no
|
||||
/// compression.
|
||||
pub fn compression_threshold(&self) -> Option<u32> {
|
||||
self.0.compression_threshold
|
||||
}
|
||||
|
||||
/// Gets the maximum number of connections allowed to the server at once.
|
||||
pub fn max_connections(&self) -> usize {
|
||||
self.0.max_connections
|
||||
}
|
||||
|
||||
/// Gets the configured incoming capacity.
|
||||
pub fn incoming_capacity(&self) -> usize {
|
||||
self.0.incoming_capacity
|
||||
}
|
||||
|
||||
/// Gets the configured outgoing incoming capacity.
|
||||
pub fn outgoing_capacity(&self) -> usize {
|
||||
self.0.outgoing_capacity
|
||||
}
|
||||
|
||||
/// Gets a handle to the tokio instance this server is using.
|
||||
pub fn tokio_handle(&self) -> &Handle {
|
||||
&self.0.tokio_handle
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains information about a new client joining the server.
|
||||
#[non_exhaustive]
|
||||
pub struct NewClientInfo {
|
||||
/// The username of the new client.
|
||||
pub username: String,
|
||||
/// The UUID of the new client.
|
||||
pub uuid: Uuid,
|
||||
/// The remote address of the new client.
|
||||
pub ip: IpAddr,
|
||||
/// The client's properties from the game profile. Typically contains a
|
||||
/// `textures` property with the skin and cape of the player.
|
||||
pub properties: Vec<Property>,
|
||||
}
|
||||
|
||||
pub fn build_plugin(
|
||||
plugin: &ServerPlugin<impl AsyncCallbacks>,
|
||||
app: &mut App,
|
||||
) -> anyhow::Result<()> {
|
||||
ensure!(
|
||||
plugin.tps > 0,
|
||||
"configured tick rate must be greater than zero"
|
||||
);
|
||||
ensure!(
|
||||
plugin.incoming_capacity > 0,
|
||||
"configured incoming packet capacity must be nonzero"
|
||||
);
|
||||
ensure!(
|
||||
plugin.outgoing_capacity > 0,
|
||||
"configured outgoing packet capacity must be nonzero"
|
||||
);
|
||||
|
||||
let rsa_key = RsaPrivateKey::new(&mut OsRng, 1024)?;
|
||||
|
||||
let public_key_der =
|
||||
rsa_der::public_key_to_der(&rsa_key.n().to_bytes_be(), &rsa_key.e().to_bytes_be())
|
||||
.into_boxed_slice();
|
||||
|
||||
let runtime = if plugin.tokio_handle.is_none() {
|
||||
Some(Runtime::new()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let tokio_handle = match &runtime {
|
||||
Some(rt) => rt.handle().clone(),
|
||||
None => plugin.tokio_handle.clone().unwrap(),
|
||||
};
|
||||
|
||||
let (new_clients_send, new_clients_recv) = flume::bounded(64);
|
||||
|
||||
let shared = SharedServer(Arc::new(SharedServerInner {
|
||||
address: plugin.address,
|
||||
tps: plugin.tps,
|
||||
connection_mode: plugin.connection_mode.clone(),
|
||||
compression_threshold: plugin.compression_threshold,
|
||||
max_connections: plugin.max_connections,
|
||||
incoming_capacity: plugin.incoming_capacity,
|
||||
outgoing_capacity: plugin.outgoing_capacity,
|
||||
tokio_handle,
|
||||
_tokio_runtime: runtime,
|
||||
new_clients_send,
|
||||
new_clients_recv,
|
||||
connection_sema: Arc::new(Semaphore::new(plugin.max_connections)),
|
||||
rsa_key,
|
||||
public_key_der,
|
||||
http_client: Default::default(),
|
||||
}));
|
||||
|
||||
let server = Server {
|
||||
current_tick: 0,
|
||||
shared,
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
// System to spawn new clients.
|
||||
let spawn_new_clients = move |world: &mut World| {
|
||||
for _ in 0..shared.0.new_clients_recv.len() {
|
||||
let Ok(args) = shared.0.new_clients_recv.try_recv() else {
|
||||
break
|
||||
};
|
||||
|
||||
world.spawn(ClientBundle::new(args.info, args.conn, args.enc));
|
||||
}
|
||||
};
|
||||
|
||||
let shared = server.shared.clone();
|
||||
|
||||
// Insert resources.
|
||||
app.insert_resource(server);
|
||||
|
||||
// Make the app loop forever at the configured TPS.
|
||||
{
|
||||
let tick_period = Duration::from_secs_f64((shared.tps() as f64).recip());
|
||||
|
||||
app.insert_resource(ScheduleRunnerSettings::run_loop(tick_period))
|
||||
.add_plugin(ScheduleRunnerPlugin);
|
||||
}
|
||||
|
||||
// Start accepting connections in `PostStartup` to allow user startup code to
|
||||
// run first.
|
||||
app.add_system(
|
||||
start_accept_loop
|
||||
.in_schedule(CoreSchedule::Startup)
|
||||
.in_base_set(StartupSet::PostStartup),
|
||||
);
|
||||
|
||||
// Spawn new clients before the event loop starts.
|
||||
app.add_system(
|
||||
spawn_new_clients
|
||||
.in_base_set(CoreSet::PreUpdate)
|
||||
.before(RunEventLoopSet),
|
||||
);
|
||||
|
||||
app.add_system(increment_tick_counter.in_base_set(CoreSet::Last));
|
||||
|
||||
// Add internal plugins.
|
||||
app.add_plugin(EventLoopPlugin)
|
||||
.add_plugin(RegistryCodecPlugin)
|
||||
.add_plugin(BiomePlugin)
|
||||
.add_plugin(DimensionPlugin)
|
||||
.add_plugin(ComponentPlugin)
|
||||
.add_plugin(ClientPlugin)
|
||||
.add_plugin(EntityPlugin)
|
||||
.add_plugin(InstancePlugin)
|
||||
.add_plugin(InventoryPlugin)
|
||||
.add_plugin(PlayerListPlugin)
|
||||
.add_plugin(WeatherPlugin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn increment_tick_counter(mut server: ResMut<Server>) {
|
||||
server.current_tick += 1;
|
||||
}
|
|
@ -6,45 +6,85 @@ use bevy_app::prelude::*;
|
|||
use bevy_ecs::prelude::*;
|
||||
use bevy_ecs::schedule::{LogLevel, ScheduleBuildSettings};
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use valence_protocol::decoder::{decode_packet, PacketDecoder};
|
||||
use valence_protocol::encoder::PacketEncoder;
|
||||
use valence_protocol::packet::S2cPlayPacket;
|
||||
use valence_protocol::var_int::VarInt;
|
||||
use valence_protocol::{ident, Packet};
|
||||
use uuid::Uuid;
|
||||
use valence_client::ClientBundleArgs;
|
||||
use valence_core::packet::decode::{decode_packet, PacketDecoder};
|
||||
use valence_core::packet::encode::PacketEncoder;
|
||||
use valence_core::packet::s2c::play::S2cPlayPacket;
|
||||
use valence_core::packet::var_int::VarInt;
|
||||
use valence_core::packet::Packet;
|
||||
use valence_core::{ident, CoreSettings, Server};
|
||||
use valence_entity::Location;
|
||||
use valence_network::{ConnectionMode, NetworkSettings};
|
||||
|
||||
use crate::client::{ClientBundle, ClientConnection, ReceivedPacket};
|
||||
use crate::component::Location;
|
||||
use crate::config::{ConnectionMode, ServerPlugin};
|
||||
use crate::instance::Instance;
|
||||
use crate::server::{NewClientInfo, Server};
|
||||
use crate::DefaultPlugins;
|
||||
|
||||
/// Sets up valence with a single mock client. Returns the Entity of the client
|
||||
/// and the corresponding MockClientHelper.
|
||||
///
|
||||
/// Reduces boilerplate in unit tests.
|
||||
fn scenario_single_client(app: &mut App) -> (Entity, MockClientHelper) {
|
||||
app.insert_resource(CoreSettings {
|
||||
compression_threshold: None,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
app.insert_resource(NetworkSettings {
|
||||
connection_mode: ConnectionMode::Offline,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
app.add_plugins(DefaultPlugins);
|
||||
|
||||
let server = app.world.resource::<Server>();
|
||||
let instance = Instance::new_unit_testing(ident!("overworld"), server);
|
||||
let instance_ent = app.world.spawn(instance).id();
|
||||
let (client, client_helper) = create_mock_client();
|
||||
|
||||
let client_ent = app.world.spawn(client).id();
|
||||
|
||||
// Set initial location.
|
||||
app.world.get_mut::<Location>(client_ent).unwrap().0 = instance_ent;
|
||||
|
||||
// Print warnings if there are ambiguities in the schedule.
|
||||
app.edit_schedule(CoreSchedule::Main, |schedule| {
|
||||
schedule.set_build_settings(ScheduleBuildSettings {
|
||||
ambiguity_detection: LogLevel::Warn,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
||||
(client_ent, client_helper)
|
||||
}
|
||||
|
||||
/// Creates a mock client bundle 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(crate) fn create_mock_client(client_info: NewClientInfo) -> (ClientBundle, MockClientHelper) {
|
||||
let mock_connection = MockClientConnection::new();
|
||||
let enc = PacketEncoder::new();
|
||||
let bundle = ClientBundle::new(client_info, Box::new(mock_connection.clone()), enc);
|
||||
fn create_mock_client() -> (ClientBundle, MockClientHelper) {
|
||||
let conn = MockClientConnection::new();
|
||||
|
||||
(bundle, MockClientHelper::new(mock_connection))
|
||||
}
|
||||
|
||||
/// Creates a `NewClientInfo` with the given username and a random UUID.
|
||||
pub fn gen_client_info(username: impl Into<String>) -> NewClientInfo {
|
||||
NewClientInfo {
|
||||
username: username.into(),
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
|
||||
let bundle = ClientBundle::new(ClientBundleArgs {
|
||||
username: "test".into(),
|
||||
uuid: Uuid::from_bytes(rand::random()),
|
||||
ip: "127.0.0.1".parse().unwrap(),
|
||||
properties: vec![],
|
||||
}
|
||||
conn: Box::new(conn.clone()),
|
||||
enc: PacketEncoder::new(),
|
||||
});
|
||||
|
||||
let helper = MockClientHelper::new(conn);
|
||||
|
||||
(bundle, helper)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
struct MockClientConnection {
|
||||
inner: Arc<Mutex<MockClientConnectionInner>>,
|
||||
}
|
||||
|
||||
|
@ -57,7 +97,7 @@ struct MockClientConnectionInner {
|
|||
}
|
||||
|
||||
impl MockClientConnection {
|
||||
pub fn new() -> Self {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(MockClientConnectionInner {
|
||||
recv_buf: VecDeque::new(),
|
||||
|
@ -67,7 +107,7 @@ impl MockClientConnection {
|
|||
}
|
||||
|
||||
/// Injects a (Packet ID + data) frame to be received by the server.
|
||||
pub fn inject_recv(&mut self, mut bytes: BytesMut) {
|
||||
fn inject_recv(&mut self, mut bytes: BytesMut) {
|
||||
let id = VarInt::decode_partial((&mut bytes).reader()).expect("failed to decode packet ID");
|
||||
|
||||
self.inner
|
||||
|
@ -81,11 +121,11 @@ impl MockClientConnection {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn take_sent(&mut self) -> BytesMut {
|
||||
fn take_sent(&mut self) -> BytesMut {
|
||||
self.inner.lock().unwrap().send_buf.split()
|
||||
}
|
||||
|
||||
pub fn clear_sent(&mut self) {
|
||||
fn clear_sent(&mut self) {
|
||||
self.inner.lock().unwrap().send_buf.clear();
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +147,7 @@ impl ClientConnection for MockClientConnection {
|
|||
|
||||
/// Contains the mocked client connection and helper methods to inject packets
|
||||
/// and read packets from the send stream.
|
||||
pub struct MockClientHelper {
|
||||
struct MockClientHelper {
|
||||
conn: MockClientConnection,
|
||||
dec: PacketDecoder,
|
||||
scratch: BytesMut,
|
||||
|
@ -126,7 +166,7 @@ impl MockClientHelper {
|
|||
|
||||
/// Inject a packet to be treated as a packet inbound to the server. Panics
|
||||
/// if the packet cannot be sent.
|
||||
pub fn send<'a>(&mut self, packet: &impl Packet<'a>) {
|
||||
fn send<'a>(&mut self, packet: &impl Packet<'a>) {
|
||||
packet
|
||||
.encode_packet((&mut self.scratch).writer())
|
||||
.expect("failed to encode packet");
|
||||
|
@ -135,7 +175,7 @@ impl MockClientHelper {
|
|||
}
|
||||
|
||||
/// Collect all packets that have been sent to the client.
|
||||
pub fn collect_sent(&mut self) -> Vec<S2cPlayPacket> {
|
||||
fn collect_sent(&mut self) -> Vec<S2cPlayPacket> {
|
||||
self.dec.queue_bytes(self.conn.take_sent());
|
||||
|
||||
self.collected_frames.clear();
|
||||
|
@ -154,47 +194,14 @@ impl MockClientHelper {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn clear_sent(&mut self) {
|
||||
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 = Instance::new_unit_testing(ident!("overworld"), server);
|
||||
let instance_ent = app.world.spawn(instance).id();
|
||||
let (client, client_helper) = create_mock_client(gen_client_info("test"));
|
||||
|
||||
let client_ent = app.world.spawn(client).id();
|
||||
|
||||
// Set initial location.
|
||||
app.world.get_mut::<Location>(client_ent).unwrap().0 = instance_ent;
|
||||
|
||||
// Print warnings if there are ambiguities in the schedule.
|
||||
app.edit_schedule(CoreSchedule::Main, |schedule| {
|
||||
schedule.set_build_settings(ScheduleBuildSettings {
|
||||
ambiguity_detection: LogLevel::Warn,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
||||
(client_ent, client_helper)
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_packet_order {
|
||||
($sent_packets:ident, $($packets:pat),+) => {{
|
||||
let sent_packets: &Vec<valence_protocol::packet::S2cPlayPacket> = &$sent_packets;
|
||||
let sent_packets: &Vec<valence_core::packet::s2c::play::S2cPlayPacket> = &$sent_packets;
|
||||
let positions = [
|
||||
$((sent_packets.iter().position(|p| matches!(p, $packets))),)*
|
||||
];
|
||||
|
@ -202,10 +209,9 @@ macro_rules! assert_packet_order {
|
|||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_packet_count {
|
||||
($sent_packets:ident, $count:tt, $packet:pat) => {{
|
||||
let sent_packets: &Vec<valence_protocol::packet::S2cPlayPacket> = &$sent_packets;
|
||||
let sent_packets: &Vec<valence_core::packet::s2c::play::S2cPlayPacket> = &$sent_packets;
|
||||
let count = sent_packets.iter().filter(|p| matches!(p, $packet)).count();
|
||||
assert_eq!(
|
||||
count,
|
||||
|
@ -222,3 +228,8 @@ macro_rules! assert_packet_count {
|
|||
);
|
||||
}};
|
||||
}
|
||||
|
||||
mod client;
|
||||
mod example;
|
||||
mod inventory;
|
||||
mod weather;
|
82
crates/valence/src/tests/client.rs
Normal file
82
crates/valence/src/tests/client.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use std::collections::BTreeSet;
|
||||
|
||||
use bevy_app::App;
|
||||
use bevy_ecs::world::EntityMut;
|
||||
use valence_client::ViewDistance;
|
||||
use valence_core::chunk_pos::ChunkView;
|
||||
use valence_core::packet::s2c::play::{ChunkDataS2c, S2cPlayPacket, UnloadChunkS2c};
|
||||
use valence_entity::Position;
|
||||
use valence_instance::Chunk;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn client_chunk_view_change() {
|
||||
let mut app = App::new();
|
||||
|
||||
let (client_ent, mut client_helper) = scenario_single_client(&mut app);
|
||||
|
||||
let mut instance = app
|
||||
.world
|
||||
.query::<&mut Instance>()
|
||||
.single_mut(&mut app.world);
|
||||
|
||||
for z in -15..15 {
|
||||
for x in -15..15 {
|
||||
instance.insert_chunk([x, z], Chunk::default());
|
||||
}
|
||||
}
|
||||
|
||||
let mut client = app.world.entity_mut(client_ent);
|
||||
|
||||
client.get_mut::<Position>().unwrap().set([8.0, 0.0, 8.0]);
|
||||
client.get_mut::<ViewDistance>().unwrap().set(6);
|
||||
|
||||
// Tick
|
||||
app.update();
|
||||
let mut client = app.world.entity_mut(client_ent);
|
||||
|
||||
let mut loaded_chunks = BTreeSet::new();
|
||||
|
||||
for pkt in client_helper.collect_sent() {
|
||||
if let S2cPlayPacket::ChunkDataS2c(ChunkDataS2c { pos, .. }) = pkt {
|
||||
assert!(loaded_chunks.insert(pos), "({pos:?})");
|
||||
}
|
||||
}
|
||||
|
||||
for pos in view(&client).iter() {
|
||||
assert!(loaded_chunks.contains(&pos), "{pos:?}");
|
||||
}
|
||||
|
||||
assert!(!loaded_chunks.is_empty());
|
||||
|
||||
// Move the client to the adjacent chunk.
|
||||
client.get_mut::<Position>().unwrap().set([24.0, 0.0, 24.0]);
|
||||
|
||||
// Tick
|
||||
app.update();
|
||||
let client = app.world.entity_mut(client_ent);
|
||||
|
||||
for pkt in client_helper.collect_sent() {
|
||||
match pkt {
|
||||
S2cPlayPacket::ChunkDataS2c(ChunkDataS2c { pos, .. }) => {
|
||||
assert!(loaded_chunks.insert(pos), "({pos:?})");
|
||||
}
|
||||
S2cPlayPacket::UnloadChunkS2c(UnloadChunkS2c { pos }) => {
|
||||
assert!(loaded_chunks.remove(&pos), "({pos:?})");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for pos in view(&client).iter() {
|
||||
assert!(loaded_chunks.contains(&pos), "{pos:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn view(client: &EntityMut) -> ChunkView {
|
||||
let chunk_pos = client.get::<Position>().unwrap().chunk_pos();
|
||||
let view_dist = client.get::<ViewDistance>().unwrap().get();
|
||||
|
||||
ChunkView::new(chunk_pos, view_dist)
|
||||
}
|
88
crates/valence/src/tests/example.rs
Normal file
88
crates/valence/src/tests/example.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
//! 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.
|
||||
|
||||
use bevy_app::App;
|
||||
use valence_core::packet::c2s::play::PositionAndOnGround;
|
||||
use valence_core::packet::s2c::play::S2cPlayPacket;
|
||||
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// The server's tick should increment every update.
|
||||
#[test]
|
||||
fn example_test_server_tick_increment() {
|
||||
let mut app = App::new();
|
||||
|
||||
app.add_plugins(DefaultPlugins);
|
||||
|
||||
let tick = app.world.resource::<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 = PositionAndOnGround {
|
||||
position: DVec3::new(12.0, 64.0, 0.0),
|
||||
on_ground: true,
|
||||
};
|
||||
client_helper.send(&packet);
|
||||
|
||||
// Process the packet.
|
||||
app.update();
|
||||
|
||||
// Make assertions
|
||||
let pos = app.world.get::<Position>(client_ent).unwrap();
|
||||
assert_eq!(pos.0, DVec3::new(12.0, 64.0, 0.0));
|
||||
}
|
||||
|
||||
/// A unit test where we want to test what packets are sent to the client.
|
||||
#[test]
|
||||
fn example_test_open_inventory() {
|
||||
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::OpenScreenS2c(_));
|
||||
assert_packet_count!(sent_packets, 1, S2cPlayPacket::InventoryS2c(_));
|
||||
assert_packet_order!(
|
||||
sent_packets,
|
||||
S2cPlayPacket::OpenScreenS2c(_),
|
||||
S2cPlayPacket::InventoryS2c(_)
|
||||
);
|
||||
}
|
1038
crates/valence/src/tests/inventory.rs
Normal file
1038
crates/valence/src/tests/inventory.rs
Normal file
File diff suppressed because it is too large
Load diff
141
crates/valence/src/tests/weather.rs
Normal file
141
crates/valence/src/tests/weather.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use bevy_app::App;
|
||||
use valence_client::weather::{Rain, Thunder};
|
||||
use valence_client::Client;
|
||||
use valence_core::packet::s2c::play::game_state_change::GameEventKind;
|
||||
use valence_core::packet::s2c::play::{GameStateChangeS2c, S2cPlayPacket};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn assert_weather_packets(sent_packets: Vec<S2cPlayPacket>) {
|
||||
assert_packet_count!(sent_packets, 6, S2cPlayPacket::GameStateChangeS2c(_));
|
||||
|
||||
assert_packet_order!(
|
||||
sent_packets,
|
||||
S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
|
||||
kind: GameEventKind::BeginRaining,
|
||||
value: _
|
||||
}),
|
||||
S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
|
||||
kind: GameEventKind::RainLevelChange,
|
||||
value: _
|
||||
}),
|
||||
S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
|
||||
kind: GameEventKind::ThunderLevelChange,
|
||||
value: _
|
||||
}),
|
||||
S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
|
||||
kind: GameEventKind::EndRaining,
|
||||
value: _
|
||||
})
|
||||
);
|
||||
|
||||
if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[1] {
|
||||
assert_eq!(pkt.value, 0.5);
|
||||
}
|
||||
|
||||
if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[2] {
|
||||
assert_eq!(pkt.value, 1.0);
|
||||
}
|
||||
|
||||
if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[3] {
|
||||
assert_eq!(pkt.value, 0.5);
|
||||
}
|
||||
|
||||
if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[4] {
|
||||
assert_eq!(pkt.value, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weather_instance() {
|
||||
let mut app = App::new();
|
||||
let (_, mut client_helper) = scenario_single_client(&mut app);
|
||||
|
||||
// Process a tick to get past the "on join" logic.
|
||||
app.update();
|
||||
client_helper.clear_sent();
|
||||
|
||||
// Get the instance entity.
|
||||
let instance_ent = app
|
||||
.world
|
||||
.iter_entities()
|
||||
.find(|e| e.contains::<Instance>())
|
||||
.expect("could not find instance")
|
||||
.id();
|
||||
|
||||
// Insert a rain component to the instance.
|
||||
app.world.entity_mut(instance_ent).insert(Rain(0.5));
|
||||
for _ in 0..2 {
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Alter a rain component of the instance.
|
||||
app.world.entity_mut(instance_ent).insert(Rain(1.0));
|
||||
app.update();
|
||||
|
||||
// Insert a thunder component to the instance.
|
||||
app.world.entity_mut(instance_ent).insert(Thunder(0.5));
|
||||
app.update();
|
||||
|
||||
// Alter a thunder component of the instance.
|
||||
app.world.entity_mut(instance_ent).insert(Thunder(1.0));
|
||||
app.update();
|
||||
|
||||
// Remove the rain component from the instance.
|
||||
app.world.entity_mut(instance_ent).remove::<Rain>();
|
||||
for _ in 0..2 {
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Make assertions.
|
||||
let sent_packets = client_helper.collect_sent();
|
||||
|
||||
assert_weather_packets(sent_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weather_client() {
|
||||
let mut app = App::new();
|
||||
let (_, mut client_helper) = scenario_single_client(&mut app);
|
||||
|
||||
// Process a tick to get past the "on join" logic.
|
||||
app.update();
|
||||
client_helper.clear_sent();
|
||||
|
||||
// Get the client entity.
|
||||
let client_ent = app
|
||||
.world
|
||||
.iter_entities()
|
||||
.find(|e| e.contains::<Client>())
|
||||
.expect("could not find client")
|
||||
.id();
|
||||
|
||||
// Insert a rain component to the client.
|
||||
app.world.entity_mut(client_ent).insert(Rain(0.5));
|
||||
for _ in 0..2 {
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Alter a rain component of the client.
|
||||
app.world.entity_mut(client_ent).insert(Rain(1.0));
|
||||
app.update();
|
||||
|
||||
// Insert a thunder component to the client.
|
||||
app.world.entity_mut(client_ent).insert(Thunder(0.5));
|
||||
app.update();
|
||||
|
||||
// Alter a thunder component of the client.
|
||||
app.world.entity_mut(client_ent).insert(Thunder(1.0));
|
||||
app.update();
|
||||
|
||||
// Remove the rain component from the client.
|
||||
app.world.entity_mut(client_ent).remove::<Rain>();
|
||||
for _ in 0..2 {
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Make assertions.
|
||||
let sent_packets = client_helper.collect_sent();
|
||||
|
||||
assert_weather_packets(sent_packets);
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
//! # 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::packet::S2cPlayPacket;
|
||||
|
||||
use super::*;
|
||||
use crate::client::Client;
|
||||
use crate::component::Position;
|
||||
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::packet::c2s::play::PositionAndOnGround {
|
||||
position: [12.0, 64.0, 0.0],
|
||||
on_ground: true,
|
||||
};
|
||||
client_helper.send(&packet);
|
||||
|
||||
// Process the packet.
|
||||
app.update();
|
||||
|
||||
// Make assertions
|
||||
let pos = app.world.get::<Position>(client_ent).unwrap();
|
||||
assert_eq!(pos.0, [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() {
|
||||
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::OpenScreenS2c(_));
|
||||
assert_packet_count!(sent_packets, 1, S2cPlayPacket::InventoryS2c(_));
|
||||
assert_packet_order!(
|
||||
sent_packets,
|
||||
S2cPlayPacket::OpenScreenS2c(_),
|
||||
S2cPlayPacket::InventoryS2c(_)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
mod example;
|
||||
pub(crate) mod util;
|
|
@ -1,123 +0,0 @@
|
|||
pub use glam::*;
|
||||
|
||||
use crate::config::DEFAULT_TPS;
|
||||
|
||||
/// 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),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
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 _
|
||||
}
|
||||
|
||||
/// Returns whether or not the given string is a valid Minecraft username.
|
||||
///
|
||||
/// A valid username is 3 to 16 characters long with only ASCII alphanumeric
|
||||
/// characters. The username must match the regex `^[a-zA-Z0-9_]{3,16}$` to be
|
||||
/// considered valid.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use valence::util::is_valid_username;
|
||||
///
|
||||
/// assert!(is_valid_username("00a"));
|
||||
/// assert!(is_valid_username("jeb_"));
|
||||
///
|
||||
/// assert!(!is_valid_username("notavalidusername"));
|
||||
/// assert!(!is_valid_username("NotValid!"));
|
||||
/// ```
|
||||
pub fn is_valid_username(username: &str) -> bool {
|
||||
(3..=16).contains(&username.len())
|
||||
&& username
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn velocity_to_packet_units(vel: Vec3) -> [i16; 3] {
|
||||
// The saturating casts to i16 are desirable.
|
||||
(8000.0 / DEFAULT_TPS as f32 * vel)
|
||||
.to_array()
|
||||
.map(|v| v as i16)
|
||||
}
|
|
@ -1,413 +0,0 @@
|
|||
//! The weather system.
|
||||
//!
|
||||
//! This module contains the systems and components needed to handle
|
||||
//! weather.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! The components may be attached to clients or instances.
|
||||
//!
|
||||
//! - [`Rain`]: When attached, raining begin and rain level set events are
|
||||
//! emitted. When removed, the end raining event is emitted.
|
||||
//! - [`Thunder`]: When attached, thunder level set event is emitted. When
|
||||
//! removed, the thunder level set to zero event is emitted.
|
||||
//!
|
||||
//! New joined players are handled, so that they are get weather events from
|
||||
//! the instance.
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use valence_protocol::packet::s2c::play::game_state_change::GameEventKind;
|
||||
use valence_protocol::packet::s2c::play::GameStateChangeS2c;
|
||||
|
||||
use crate::client::FlushPacketsSet;
|
||||
use crate::instance::WriteUpdatePacketsToInstancesSet;
|
||||
use crate::packet::WritePacket;
|
||||
use crate::prelude::*;
|
||||
|
||||
pub const WEATHER_LEVEL: Range<f32> = 0_f32..1_f32;
|
||||
|
||||
/// Contains the rain level.
|
||||
/// Valid value is a value within the [WEATHER_LEVEL] range.
|
||||
/// Invalid value would be clamped.
|
||||
#[derive(Component)]
|
||||
pub struct Rain(pub f32);
|
||||
|
||||
/// Contains the thunder level.
|
||||
/// Valid value is a value within the [WEATHER_LEVEL] range.
|
||||
/// Invalid value would be clamped.
|
||||
#[derive(Component)]
|
||||
pub struct Thunder(pub f32);
|
||||
|
||||
impl Instance {
|
||||
/// Sends the begin rain event to all players in the instance.
|
||||
fn begin_raining(&mut self) {
|
||||
self.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::BeginRaining,
|
||||
value: f32::default(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends the end rain event to all players in the instance.
|
||||
fn end_raining(&mut self) {
|
||||
self.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::EndRaining,
|
||||
value: f32::default(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends the set rain level event to all players in the instance.
|
||||
fn set_rain_level(&mut self, level: f32) {
|
||||
self.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::RainLevelChange,
|
||||
value: level.clamp(WEATHER_LEVEL.start, WEATHER_LEVEL.end),
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends the set thunder level event to all players in the instance.
|
||||
fn set_thunder_level(&mut self, level: f32) {
|
||||
self.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::ThunderLevelChange,
|
||||
value: level.clamp(WEATHER_LEVEL.start, WEATHER_LEVEL.end),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Sends the begin rain event to the client.
|
||||
fn begin_raining(&mut self) {
|
||||
self.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::BeginRaining,
|
||||
value: f32::default(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends the end rain event to the client.
|
||||
fn end_raining(&mut self) {
|
||||
self.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::EndRaining,
|
||||
value: f32::default(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends the set rain level event to the client.
|
||||
fn set_rain_level(&mut self, level: f32) {
|
||||
self.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::RainLevelChange,
|
||||
value: level.clamp(WEATHER_LEVEL.start, WEATHER_LEVEL.end),
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends the set thunder level event to the client.
|
||||
fn set_thunder_level(&mut self, level: f32) {
|
||||
self.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::ThunderLevelChange,
|
||||
value: level.clamp(WEATHER_LEVEL.start, WEATHER_LEVEL.end),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_weather_for_joined_player(
|
||||
mut clients: Query<(&mut Client, &Location), Added<Client>>,
|
||||
weathers: Query<(Option<&Rain>, Option<&Thunder>), With<Instance>>,
|
||||
) {
|
||||
clients.for_each_mut(|(mut client, loc)| {
|
||||
if let Ok((rain, thunder)) = weathers.get(loc.0) {
|
||||
if let Some(level) = rain {
|
||||
client.begin_raining();
|
||||
client.set_rain_level(level.0);
|
||||
}
|
||||
|
||||
if let Some(level) = thunder {
|
||||
client.set_thunder_level(level.0);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_rain_begin_per_instance(mut query: Query<&mut Instance, Added<Rain>>) {
|
||||
query.for_each_mut(|mut instance| {
|
||||
instance.begin_raining();
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_rain_change_per_instance(mut query: Query<(&mut Instance, &Rain), Changed<Rain>>) {
|
||||
query.for_each_mut(|(mut instance, rain)| instance.set_rain_level(rain.0));
|
||||
}
|
||||
|
||||
fn handle_rain_end_per_instance(
|
||||
mut query: Query<&mut Instance>,
|
||||
mut removed: RemovedComponents<Rain>,
|
||||
) {
|
||||
removed.iter().for_each(|entity| {
|
||||
if let Ok(mut instance) = query.get_mut(entity) {
|
||||
instance.end_raining();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_thunder_change_per_instance(
|
||||
mut query: Query<(&mut Instance, &Thunder), Changed<Thunder>>,
|
||||
) {
|
||||
query.for_each_mut(|(mut instance, thunder)| instance.set_thunder_level(thunder.0));
|
||||
}
|
||||
|
||||
fn handle_thunder_end_per_instance(
|
||||
mut query: Query<&mut Instance>,
|
||||
mut removed: RemovedComponents<Thunder>,
|
||||
) {
|
||||
removed.iter().for_each(|entity| {
|
||||
if let Ok(mut instance) = query.get_mut(entity) {
|
||||
instance.set_thunder_level(WEATHER_LEVEL.start);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_rain_begin_per_client(mut query: Query<&mut Client, (Added<Rain>, Without<Instance>)>) {
|
||||
query.for_each_mut(|mut client| {
|
||||
client.begin_raining();
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_rain_change_per_client(
|
||||
mut query: Query<(&mut Client, &Rain), (Changed<Rain>, Without<Instance>)>,
|
||||
) {
|
||||
query.for_each_mut(|(mut client, rain)| {
|
||||
client.set_rain_level(rain.0);
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_rain_end_per_client(mut query: Query<&mut Client>, mut removed: RemovedComponents<Rain>) {
|
||||
removed.iter().for_each(|entity| {
|
||||
if let Ok(mut client) = query.get_mut(entity) {
|
||||
client.end_raining();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_thunder_change_per_client(
|
||||
mut query: Query<(&mut Client, &Thunder), (Changed<Thunder>, Without<Instance>)>,
|
||||
) {
|
||||
query.for_each_mut(|(mut client, thunder)| {
|
||||
client.set_thunder_level(thunder.0);
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_thunder_end_per_client(
|
||||
mut query: Query<&mut Client, Without<Instance>>,
|
||||
mut removed: RemovedComponents<Thunder>,
|
||||
) {
|
||||
removed.iter().for_each(|entity| {
|
||||
if let Ok(mut client) = query.get_mut(entity) {
|
||||
client.set_thunder_level(WEATHER_LEVEL.start);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) struct WeatherPlugin;
|
||||
|
||||
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub(crate) struct UpdateWeatherPerInstanceSet;
|
||||
|
||||
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub(crate) struct UpdateWeatherPerClientSet;
|
||||
|
||||
impl Plugin for WeatherPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.configure_set(
|
||||
UpdateWeatherPerInstanceSet
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.before(WriteUpdatePacketsToInstancesSet),
|
||||
)
|
||||
.configure_set(
|
||||
UpdateWeatherPerClientSet
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.before(FlushPacketsSet),
|
||||
)
|
||||
.add_systems(
|
||||
(
|
||||
handle_rain_begin_per_instance,
|
||||
handle_rain_change_per_instance,
|
||||
handle_rain_end_per_instance,
|
||||
handle_thunder_change_per_instance,
|
||||
handle_thunder_end_per_instance,
|
||||
)
|
||||
.chain()
|
||||
.in_set(UpdateWeatherPerInstanceSet)
|
||||
.before(UpdateWeatherPerClientSet),
|
||||
)
|
||||
.add_systems(
|
||||
(
|
||||
handle_rain_begin_per_client,
|
||||
handle_rain_change_per_client,
|
||||
handle_rain_end_per_client,
|
||||
handle_thunder_change_per_client,
|
||||
handle_thunder_end_per_client,
|
||||
)
|
||||
.chain()
|
||||
.in_set(UpdateWeatherPerClientSet),
|
||||
)
|
||||
.add_system(
|
||||
handle_weather_for_joined_player
|
||||
.before(UpdateWeatherPerClientSet)
|
||||
.in_base_set(CoreSet::PostUpdate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use bevy_app::App;
|
||||
use valence_protocol::packet::S2cPlayPacket;
|
||||
|
||||
use super::*;
|
||||
use crate::unit_test::util::scenario_single_client;
|
||||
use crate::{assert_packet_count, assert_packet_order};
|
||||
|
||||
fn assert_weather_packets(sent_packets: Vec<S2cPlayPacket>) {
|
||||
assert_packet_count!(sent_packets, 6, S2cPlayPacket::GameStateChangeS2c(_));
|
||||
|
||||
assert_packet_order!(
|
||||
sent_packets,
|
||||
S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
|
||||
kind: GameEventKind::BeginRaining,
|
||||
value: _
|
||||
}),
|
||||
S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
|
||||
kind: GameEventKind::RainLevelChange,
|
||||
value: _
|
||||
}),
|
||||
S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
|
||||
kind: GameEventKind::ThunderLevelChange,
|
||||
value: _
|
||||
}),
|
||||
S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
|
||||
kind: GameEventKind::EndRaining,
|
||||
value: _
|
||||
})
|
||||
);
|
||||
|
||||
if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[1] {
|
||||
assert_eq!(pkt.value, 0.5f32);
|
||||
}
|
||||
|
||||
if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[2] {
|
||||
assert_eq!(pkt.value, WEATHER_LEVEL.end);
|
||||
}
|
||||
|
||||
if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[3] {
|
||||
assert_eq!(pkt.value, 0.5f32);
|
||||
}
|
||||
|
||||
if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[4] {
|
||||
assert_eq!(pkt.value, WEATHER_LEVEL.end);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weather_instance() {
|
||||
let mut app = App::new();
|
||||
let (_, mut client_helper) = scenario_single_client(&mut app);
|
||||
|
||||
// Process a tick to get past the "on join" logic.
|
||||
app.update();
|
||||
client_helper.clear_sent();
|
||||
|
||||
// Get the instance entity.
|
||||
let instance_ent = app
|
||||
.world
|
||||
.iter_entities()
|
||||
.find(|e| e.contains::<Instance>())
|
||||
.expect("could not find instance")
|
||||
.id();
|
||||
|
||||
// Insert a rain component to the instance.
|
||||
app.world.entity_mut(instance_ent).insert(Rain(0.5f32));
|
||||
for _ in 0..2 {
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Alter a rain component of the instance.
|
||||
app.world.entity_mut(instance_ent).insert(Rain(
|
||||
// Invalid value to assert it is clamped.
|
||||
WEATHER_LEVEL.end + 1_f32,
|
||||
));
|
||||
app.update();
|
||||
|
||||
// Insert a thunder component to the instance.
|
||||
app.world.entity_mut(instance_ent).insert(Thunder(0.5f32));
|
||||
app.update();
|
||||
|
||||
// Alter a thunder component of the instance.
|
||||
app.world.entity_mut(instance_ent).insert(Thunder(
|
||||
// Invalid value to assert it is clamped.
|
||||
WEATHER_LEVEL.end + 1_f32,
|
||||
));
|
||||
app.update();
|
||||
|
||||
// Remove the rain component from the instance.
|
||||
app.world.entity_mut(instance_ent).remove::<Rain>();
|
||||
for _ in 0..2 {
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Make assertions.
|
||||
let sent_packets = client_helper.collect_sent();
|
||||
|
||||
assert_weather_packets(sent_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weather_client() {
|
||||
let mut app = App::new();
|
||||
let (_, mut client_helper) = scenario_single_client(&mut app);
|
||||
|
||||
// Process a tick to get past the "on join" logic.
|
||||
app.update();
|
||||
client_helper.clear_sent();
|
||||
|
||||
// Get the client entity.
|
||||
let client_ent = app
|
||||
.world
|
||||
.iter_entities()
|
||||
.find(|e| e.contains::<Client>())
|
||||
.expect("could not find client")
|
||||
.id();
|
||||
|
||||
// Insert a rain component to the client.
|
||||
app.world.entity_mut(client_ent).insert(Rain(0.5f32));
|
||||
for _ in 0..2 {
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Alter a rain component of the client.
|
||||
app.world.entity_mut(client_ent).insert(Rain(
|
||||
// Invalid value to assert it is clamped.
|
||||
WEATHER_LEVEL.end + 1_f32,
|
||||
));
|
||||
app.update();
|
||||
|
||||
// Insert a thunder component to the client.
|
||||
app.world.entity_mut(client_ent).insert(Thunder(0.5f32));
|
||||
app.update();
|
||||
|
||||
// Alter a thunder component of the client.
|
||||
app.world.entity_mut(client_ent).insert(Thunder(
|
||||
// Invalid value to assert it is clamped.
|
||||
WEATHER_LEVEL.end + 1_f32,
|
||||
));
|
||||
app.update();
|
||||
|
||||
// Remove the rain component from the client.
|
||||
app.world.entity_mut(client_ent).remove::<Rain>();
|
||||
for _ in 0..2 {
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Make assertions.
|
||||
let sent_packets = client_helper.collect_sent();
|
||||
|
||||
assert_weather_packets(sent_packets);
|
||||
}
|
||||
}
|
|
@ -1,45 +1,21 @@
|
|||
[package]
|
||||
name = "valence_anvil"
|
||||
description = "A library for Minecraft's Anvil world format."
|
||||
documentation = "https://docs.rs/valence_anvil/"
|
||||
documentation.workspace = true
|
||||
repository = "https://github.com/valence-rs/valence/tree/main/crates/valence_anvil"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
license.workspace = true
|
||||
keywords = ["anvil", "minecraft", "deserialization"]
|
||||
version = "0.1.0"
|
||||
authors = ["Ryan Johnson <ryanj00a@gmail.com>", "TerminatorNL <TerminatorNL@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1.4.3"
|
||||
flate2 = "1.0.25"
|
||||
thiserror = "1.0.37"
|
||||
num-integer = "0.1.45" # TODO: remove when div_ceil is stabilized.
|
||||
valence = { version = "0.2.0", path = "../valence", optional = true }
|
||||
valence_nbt = { version = "0.5.0", path = "../valence_nbt" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.68"
|
||||
bevy_ecs = "0.10"
|
||||
clap = { version = "4.1.4", features = ["derive"] }
|
||||
criterion = "0.4.0"
|
||||
flume = "0.10.14"
|
||||
fs_extra = "1.2.0"
|
||||
tempfile = "3.3.0"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = "0.3.16"
|
||||
zip = "0.6.3"
|
||||
valence_protocol = { version = "0.1.0", path = "../valence_protocol" }
|
||||
|
||||
[dev-dependencies.reqwest]
|
||||
version = "0.11.12"
|
||||
default-features = false
|
||||
# Avoid OpenSSL dependency on Linux.
|
||||
features = ["rustls-tls", "blocking", "stream"]
|
||||
|
||||
[[bench]]
|
||||
name = "world_parsing"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = ["valence"]
|
||||
byteorder.workspace = true
|
||||
flate2.workspace = true
|
||||
num-integer.workspace = true
|
||||
thiserror.workspace = true
|
||||
valence_biome.workspace = true
|
||||
valence_block.workspace = true
|
||||
valence_core.workspace = true
|
||||
valence_instance.workspace = true
|
||||
valence_nbt.workspace = true
|
||||
|
|
3
crates/valence_anvil/README.md
Normal file
3
crates/valence_anvil/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# valence_anvil
|
||||
|
||||
Support for Minecraft's [anvil file format](https://minecraft.fandom.com/wiki/Anvil_file_format).
|
|
@ -1,3 +1,22 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![deny(
|
||||
rustdoc::broken_intra_doc_links,
|
||||
rustdoc::private_intra_doc_links,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
rustdoc::invalid_codeblock_attributes,
|
||||
rustdoc::invalid_rust_codeblocks,
|
||||
rustdoc::bare_urls,
|
||||
rustdoc::invalid_html_tags
|
||||
)]
|
||||
#![warn(
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unused_lifetimes,
|
||||
unused_import_braces,
|
||||
unreachable_pub,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
|
||||
use std::collections::btree_map::Entry;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::File;
|
||||
|
@ -8,11 +27,9 @@ use std::path::PathBuf;
|
|||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use flate2::bufread::{GzDecoder, ZlibDecoder};
|
||||
use thiserror::Error;
|
||||
#[cfg(feature = "valence")]
|
||||
pub use to_valence::*;
|
||||
use valence_nbt::Compound;
|
||||
|
||||
#[cfg(feature = "valence")]
|
||||
mod to_valence;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -2,10 +2,10 @@ use std::borrow::Cow;
|
|||
|
||||
use num_integer::{div_ceil, Integer};
|
||||
use thiserror::Error;
|
||||
use valence::biome::BiomeId;
|
||||
use valence::instance::{BlockEntity, Chunk};
|
||||
use valence::protocol::block::{BlockEntityKind, BlockKind, PropName, PropValue};
|
||||
use valence::protocol::ident::Ident;
|
||||
use valence_biome::BiomeId;
|
||||
use valence_block::{BlockEntityKind, BlockKind, PropName, PropValue};
|
||||
use valence_core::ident::Ident;
|
||||
use valence_instance::{BlockEntity, Chunk};
|
||||
use valence_nbt::{Compound, List, Value};
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
|
|
13
crates/valence_biome/Cargo.toml
Normal file
13
crates/valence_biome/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "valence_biome"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bevy_app.workspace = true
|
||||
bevy_ecs.workspace = true
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
valence_nbt.workspace = true
|
||||
valence_registry.workspace = true
|
||||
valence_core.workspace = true
|
10
crates/valence_biome/README.md
Normal file
10
crates/valence_biome/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# valence_biome
|
||||
|
||||
Contains biomes and the biome registry. Minecraft's default biomes are added to the registry by default.
|
||||
|
||||
### **NOTE:**
|
||||
- Modifying the biome registry after the server has started can
|
||||
break invariants within instances and clients! Make sure there are no
|
||||
instances or clients spawned before mutating.
|
||||
- A biome named "minecraft:plains" must exist. Otherwise, vanilla clients
|
||||
will be disconnected.
|
|
@ -1,102 +1,53 @@
|
|||
//! Biome configuration and identification.
|
||||
//!
|
||||
//! **NOTE:**
|
||||
//!
|
||||
//! - Modifying the biome registry after the server has started can
|
||||
//! break invariants within instances and clients! Make sure there are no
|
||||
//! instances or clients spawned before mutating.
|
||||
//! - A biome named "minecraft:plains" must exist. Otherwise, vanilla clients
|
||||
//! will be disconnected. A biome named "minecraft:plains" is added by
|
||||
//! default.
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(
|
||||
rustdoc::broken_intra_doc_links,
|
||||
rustdoc::private_intra_doc_links,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
rustdoc::invalid_codeblock_attributes,
|
||||
rustdoc::invalid_rust_codeblocks,
|
||||
rustdoc::bare_urls,
|
||||
rustdoc::invalid_html_tags
|
||||
)]
|
||||
#![warn(
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unused_lifetimes,
|
||||
unused_import_braces,
|
||||
unreachable_pub,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
|
||||
use std::ops::Index;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use bevy_app::{CoreSet, Plugin, StartupSet};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use tracing::error;
|
||||
use valence_core::ident;
|
||||
use valence_core::ident::Ident;
|
||||
use valence_nbt::{compound, Value};
|
||||
use valence_protocol::ident;
|
||||
use valence_protocol::ident::Ident;
|
||||
use valence_registry::{RegistryCodec, RegistryCodecSet, RegistryValue};
|
||||
|
||||
use crate::registry_codec::{RegistryCodec, RegistryCodecSet, RegistryValue};
|
||||
pub struct BiomePlugin;
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct BiomeRegistry {
|
||||
id_to_biome: Vec<Entity>,
|
||||
}
|
||||
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
|
||||
impl BiomeRegistry {
|
||||
pub const KEY: Ident<&str> = ident!("minecraft:worldgen/biome");
|
||||
|
||||
pub fn get_by_id(&self, id: BiomeId) -> Option<Entity> {
|
||||
self.id_to_biome.get(id.0 as usize).cloned()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (BiomeId, Entity)> + '_ {
|
||||
self.id_to_biome
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, biome)| (BiomeId(id as _), *biome))
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<BiomeId> for BiomeRegistry {
|
||||
type Output = Entity;
|
||||
|
||||
fn index(&self, index: BiomeId) -> &Self::Output {
|
||||
self.id_to_biome
|
||||
.get(index.0 as usize)
|
||||
.unwrap_or_else(|| panic!("invalid {index:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// An index into the biome registry.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
|
||||
pub struct BiomeId(pub u16);
|
||||
|
||||
#[derive(Component, Clone, Debug)]
|
||||
pub struct Biome {
|
||||
pub name: Ident<String>,
|
||||
pub downfall: f32,
|
||||
pub fog_color: i32,
|
||||
pub sky_color: i32,
|
||||
pub water_color: i32,
|
||||
pub water_fog_color: i32,
|
||||
pub grass_color: Option<i32>,
|
||||
pub has_precipitation: bool,
|
||||
pub temperature: f32,
|
||||
// TODO: more stuff.
|
||||
}
|
||||
|
||||
impl Default for Biome {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: ident!("plains").into(),
|
||||
downfall: 0.4,
|
||||
fog_color: 12638463,
|
||||
sky_color: 7907327,
|
||||
water_color: 4159204,
|
||||
water_fog_color: 329011,
|
||||
grass_color: None,
|
||||
has_precipitation: true,
|
||||
temperature: 0.8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct BiomePlugin;
|
||||
struct BiomeSet;
|
||||
|
||||
impl Plugin for BiomePlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.insert_resource(BiomeRegistry {
|
||||
id_to_biome: vec![],
|
||||
})
|
||||
.configure_set(
|
||||
BiomeSet
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.before(RegistryCodecSet),
|
||||
)
|
||||
.add_systems(
|
||||
(update_biome_registry, remove_biomes_from_registry)
|
||||
.chain()
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.before(RegistryCodecSet),
|
||||
.in_set(BiomeSet),
|
||||
)
|
||||
.add_startup_system(load_default_biomes.in_base_set(StartupSet::PreStartup));
|
||||
}
|
||||
|
@ -238,3 +189,67 @@ fn remove_biomes_from_registry(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct BiomeRegistry {
|
||||
id_to_biome: Vec<Entity>,
|
||||
}
|
||||
|
||||
impl BiomeRegistry {
|
||||
pub const KEY: Ident<&str> = ident!("minecraft:worldgen/biome");
|
||||
|
||||
pub fn get_by_id(&self, id: BiomeId) -> Option<Entity> {
|
||||
self.id_to_biome.get(id.0 as usize).cloned()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (BiomeId, Entity)> + '_ {
|
||||
self.id_to_biome
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, biome)| (BiomeId(id as _), *biome))
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<BiomeId> for BiomeRegistry {
|
||||
type Output = Entity;
|
||||
|
||||
fn index(&self, index: BiomeId) -> &Self::Output {
|
||||
self.id_to_biome
|
||||
.get(index.0 as usize)
|
||||
.unwrap_or_else(|| panic!("invalid {index:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// An index into the biome registry.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
|
||||
pub struct BiomeId(pub u16);
|
||||
|
||||
#[derive(Component, Clone, Debug)]
|
||||
pub struct Biome {
|
||||
pub name: Ident<String>,
|
||||
pub downfall: f32,
|
||||
pub fog_color: i32,
|
||||
pub sky_color: i32,
|
||||
pub water_color: i32,
|
||||
pub water_fog_color: i32,
|
||||
pub grass_color: Option<i32>,
|
||||
pub has_precipitation: bool,
|
||||
pub temperature: f32,
|
||||
// TODO: more stuff.
|
||||
}
|
||||
|
||||
impl Default for Biome {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: ident!("plains").into(),
|
||||
downfall: 0.4,
|
||||
fog_color: 12638463,
|
||||
sky_color: 7907327,
|
||||
water_color: 4159204,
|
||||
water_fog_color: 329011,
|
||||
grass_color: None,
|
||||
has_precipitation: true,
|
||||
temperature: 0.8,
|
||||
}
|
||||
}
|
||||
}
|
18
crates/valence_block/Cargo.toml
Normal file
18
crates/valence_block/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "valence_block"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
valence_core.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
anyhow.workspace = true
|
||||
heck.workspace = true
|
||||
proc-macro2.workspace = true
|
||||
quote.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
valence_build_utils.workspace = true
|
||||
valence_core.workspace = true
|
3
crates/valence_block/README.md
Normal file
3
crates/valence_block/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# valence_block
|
||||
|
||||
Everything related to Minecraft blocks. Primarily concerned with the [`BlockState`] type.
|
|
@ -4,8 +4,7 @@ use heck::{ToPascalCase, ToShoutySnakeCase};
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ident;
|
||||
use valence_build_utils::{ident, rerun_if_changed, write_generated_file};
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
struct TopLevel {
|
||||
|
@ -69,12 +68,18 @@ struct Shape {
|
|||
max_z: f64,
|
||||
}
|
||||
|
||||
pub fn build() -> anyhow::Result<TokenStream> {
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
rerun_if_changed(["../../extracted/blocks.json"]);
|
||||
|
||||
write_generated_file(build()?, "block.rs")
|
||||
}
|
||||
|
||||
fn build() -> anyhow::Result<TokenStream> {
|
||||
let TopLevel {
|
||||
blocks,
|
||||
shapes,
|
||||
block_entity_types,
|
||||
} = serde_json::from_str(include_str!("../../../extracted/blocks.json"))?;
|
||||
} = serde_json::from_str(include_str!("../../extracted/blocks.json"))?;
|
||||
|
||||
let max_state_id = blocks.iter().map(|b| b.max_state_id()).max().unwrap();
|
||||
|
||||
|
@ -948,13 +953,13 @@ pub fn build() -> anyhow::Result<TokenStream> {
|
|||
}
|
||||
|
||||
impl Encode for BlockEntityKind {
|
||||
fn encode(&self, w: impl Write) -> Result<()> {
|
||||
fn encode(&self, w: impl Write) -> anyhow::Result<()> {
|
||||
VarInt(self.id() as i32).encode(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Decode<'a> for BlockEntityKind {
|
||||
fn decode(r: &mut &'a [u8]) -> Result<Self> {
|
||||
fn decode(r: &mut &'a [u8]) -> anyhow::Result<Self> {
|
||||
let id = VarInt::decode(r)?;
|
||||
Self::from_id(id.0 as u32).with_context(|| format!("id {}", id.0))
|
||||
}
|
|
@ -1,4 +1,22 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![allow(clippy::all)] // TODO: block build script creates many warnings.
|
||||
#![deny(
|
||||
rustdoc::broken_intra_doc_links,
|
||||
rustdoc::private_intra_doc_links,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
rustdoc::invalid_codeblock_attributes,
|
||||
rustdoc::invalid_rust_codeblocks,
|
||||
rustdoc::bare_urls,
|
||||
rustdoc::invalid_html_tags
|
||||
)]
|
||||
#![warn(
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unused_lifetimes,
|
||||
unused_import_braces,
|
||||
unreachable_pub,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
|
@ -6,11 +24,11 @@ use std::io::Write;
|
|||
use std::iter::FusedIterator;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::ident::Ident;
|
||||
use crate::item::ItemKind;
|
||||
use crate::var_int::VarInt;
|
||||
use crate::{ident, Decode, Encode, Result};
|
||||
use valence_core::ident;
|
||||
use valence_core::ident::Ident;
|
||||
use valence_core::item::ItemKind;
|
||||
use valence_core::packet::var_int::VarInt;
|
||||
use valence_core::packet::{Decode, Encode};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/block.rs"));
|
||||
|
||||
|
@ -53,13 +71,13 @@ fn fmt_block_state(bs: BlockState, f: &mut fmt::Formatter) -> fmt::Result {
|
|||
}
|
||||
|
||||
impl Encode for BlockState {
|
||||
fn encode(&self, w: impl Write) -> Result<()> {
|
||||
fn encode(&self, w: impl Write) -> anyhow::Result<()> {
|
||||
VarInt(self.to_raw() as i32).encode(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_> for BlockState {
|
||||
fn decode(r: &mut &[u8]) -> Result<Self> {
|
||||
fn decode(r: &mut &[u8]) -> anyhow::Result<Self> {
|
||||
let id = VarInt::decode(r)?.0;
|
||||
let errmsg = "invalid block state ID";
|
||||
|
||||
|
@ -68,13 +86,13 @@ impl Decode<'_> for BlockState {
|
|||
}
|
||||
|
||||
impl Encode for BlockKind {
|
||||
fn encode(&self, w: impl Write) -> Result<()> {
|
||||
fn encode(&self, w: impl Write) -> anyhow::Result<()> {
|
||||
VarInt(self.to_raw() as i32).encode(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_> for BlockKind {
|
||||
fn decode(r: &mut &[u8]) -> Result<Self> {
|
||||
fn decode(r: &mut &[u8]) -> anyhow::Result<Self> {
|
||||
let id = VarInt::decode(r)?.0;
|
||||
let errmsg = "invalid block kind ID";
|
||||
|
9
crates/valence_build_utils/Cargo.toml
Normal file
9
crates/valence_build_utils/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "valence_build_utils"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
syn.workspace = true
|
||||
proc-macro2.workspace = true
|
3
crates/valence_build_utils/README.md
Normal file
3
crates/valence_build_utils/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# valence_build_utils
|
||||
|
||||
Common code used in build scripts.
|
59
crates/valence_build_utils/src/lib.rs
Normal file
59
crates/valence_build_utils/src/lib.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![deny(
|
||||
rustdoc::broken_intra_doc_links,
|
||||
rustdoc::private_intra_doc_links,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
rustdoc::invalid_codeblock_attributes,
|
||||
rustdoc::invalid_rust_codeblocks,
|
||||
rustdoc::bare_urls,
|
||||
rustdoc::invalid_html_tags
|
||||
)]
|
||||
#![warn(
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unused_lifetimes,
|
||||
unused_import_braces,
|
||||
unreachable_pub,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::{env, fs};
|
||||
|
||||
use anyhow::Context;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
|
||||
pub fn write_generated_file(content: TokenStream, out_file: &str) -> anyhow::Result<()> {
|
||||
let out_dir = env::var_os("OUT_DIR").context("failed to get OUT_DIR env var")?;
|
||||
let path = Path::new(&out_dir).join(out_file);
|
||||
let code = content.to_string();
|
||||
|
||||
fs::write(&path, code)?;
|
||||
|
||||
// Try to format the output for debugging purposes.
|
||||
// Doesn't matter if rustfmt is unavailable.
|
||||
let _ = Command::new("rustfmt").arg(path).output();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ident(s: impl AsRef<str>) -> Ident {
|
||||
let s = s.as_ref().trim();
|
||||
|
||||
// Parse the ident from a str. If the string is a Rust keyword, stick an
|
||||
// underscore in front.
|
||||
syn::parse_str::<Ident>(s)
|
||||
.unwrap_or_else(|_| Ident::new(format!("_{s}").as_str(), Span::call_site()))
|
||||
}
|
||||
|
||||
pub fn rerun_if_changed<const N: usize>(files: [&str; N]) {
|
||||
for file in files {
|
||||
assert!(
|
||||
Path::new(file).exists(),
|
||||
"File \"{file}\" does not exist. Did you forget to update the path?"
|
||||
);
|
||||
|
||||
println!("cargo:rerun-if-changed={file}");
|
||||
}
|
||||
}
|
21
crates/valence_client/Cargo.toml
Normal file
21
crates/valence_client/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "valence_client"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
bevy_app.workspace = true
|
||||
bevy_ecs.workspace = true
|
||||
bytes.workspace = true
|
||||
glam.workspace = true
|
||||
rand.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
valence_biome.workspace = true
|
||||
valence_core.workspace = true
|
||||
valence_dimension.workspace = true
|
||||
valence_entity.workspace = true
|
||||
valence_instance.workspace = true
|
||||
valence_registry.workspace = true
|
||||
|
5
crates/valence_client/README.md
Normal file
5
crates/valence_client/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# valence_client
|
||||
|
||||
Manages core components and systems related to Minecraft clients.
|
||||
|
||||
A client is a Minecraft player entity backed by an abstract network connection and other necessary components. See [`Client`] for more information.
|
|
@ -1,7 +1,7 @@
|
|||
use valence_protocol::block_pos::BlockPos;
|
||||
use valence_protocol::packet::c2s::play::player_action::Action;
|
||||
use valence_protocol::packet::c2s::play::PlayerActionC2s;
|
||||
use valence_protocol::types::Direction;
|
||||
use valence_core::block_pos::BlockPos;
|
||||
use valence_core::direction::Direction;
|
||||
use valence_core::packet::c2s::play::player_action::Action;
|
||||
use valence_core::packet::c2s::play::PlayerActionC2s;
|
||||
|
||||
use super::*;
|
||||
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
|
||||
|
@ -13,11 +13,7 @@ pub(super) fn build(app: &mut App) {
|
|||
.in_schedule(EventLoopSchedule)
|
||||
.in_base_set(EventLoopSet::PreUpdate),
|
||||
)
|
||||
.add_system(
|
||||
acknowledge_player_actions
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.before(FlushPacketsSet),
|
||||
);
|
||||
.add_system(acknowledge_player_actions.in_set(UpdateClientsSet));
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
|
@ -1,10 +1,10 @@
|
|||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use valence_protocol::packet::c2s::play::client_command::Action;
|
||||
use valence_protocol::packet::c2s::play::ClientCommandC2s;
|
||||
use valence_core::packet::c2s::play::client_command::Action;
|
||||
use valence_core::packet::c2s::play::ClientCommandC2s;
|
||||
use valence_entity::entity::Flags;
|
||||
use valence_entity::{entity, Pose};
|
||||
|
||||
use crate::entity::entity::Flags;
|
||||
use crate::entity::{entity, Pose};
|
||||
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
|
||||
|
||||
pub(super) fn build(app: &mut App) {
|
|
@ -6,15 +6,16 @@ use bevy_ecs::schedule::ScheduleLabel;
|
|||
use bevy_ecs::system::SystemState;
|
||||
use bytes::Bytes;
|
||||
use tracing::{debug, warn};
|
||||
use valence_protocol::{Decode, Packet};
|
||||
use valence_core::packet::{Decode, Packet};
|
||||
|
||||
use crate::client::Client;
|
||||
use crate::{Client, SpawnClientsSet};
|
||||
|
||||
pub(crate) struct EventLoopPlugin;
|
||||
|
||||
impl Plugin for EventLoopPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.configure_set(RunEventLoopSet.in_base_set(CoreSet::PreUpdate))
|
||||
pub(super) fn build(app: &mut App) {
|
||||
app.configure_set(
|
||||
RunEventLoopSet
|
||||
.in_base_set(CoreSet::PreUpdate)
|
||||
.after(SpawnClientsSet),
|
||||
)
|
||||
.add_system(run_event_loop.in_set(RunEventLoopSet))
|
||||
.add_event::<PacketEvent>();
|
||||
|
||||
|
@ -28,7 +29,6 @@ impl Plugin for EventLoopPlugin {
|
|||
));
|
||||
|
||||
app.add_schedule(EventLoopSchedule, event_loop);
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`ScheduleLabel`] for the event loop [`Schedule`].
|
||||
|
@ -97,7 +97,8 @@ impl PacketEvent {
|
|||
}
|
||||
|
||||
/// An exclusive system for running the event loop schedule.
|
||||
pub(crate) fn run_event_loop(
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn run_event_loop(
|
||||
world: &mut World,
|
||||
state: &mut SystemState<(
|
||||
Query<(Entity, &mut Client)>,
|
|
@ -1,9 +1,9 @@
|
|||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use valence_protocol::packet::c2s::play::player_interact_entity::EntityInteraction;
|
||||
use valence_protocol::packet::c2s::play::PlayerInteractEntityC2s;
|
||||
use valence_core::packet::c2s::play::player_interact_entity::EntityInteraction;
|
||||
use valence_core::packet::c2s::play::PlayerInteractEntityC2s;
|
||||
use valence_entity::EntityManager;
|
||||
|
||||
use crate::entity::EntityManager;
|
||||
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
|
||||
|
||||
pub(super) fn build(app: &mut App) {
|
|
@ -1,14 +1,11 @@
|
|||
use valence_protocol::packet::c2s::play::KeepAliveC2s;
|
||||
use valence_core::packet::c2s::play::KeepAliveC2s;
|
||||
use valence_core::CoreSettings;
|
||||
|
||||
use super::*;
|
||||
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
|
||||
|
||||
pub(super) fn build(app: &mut App) {
|
||||
app.add_system(
|
||||
send_keepalive
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.before(FlushPacketsSet),
|
||||
)
|
||||
app.add_system(send_keepalive.in_set(UpdateClientsSet))
|
||||
.add_system(
|
||||
handle_keepalive_response
|
||||
.in_base_set(EventLoopSet::PreUpdate)
|
||||
|
@ -36,9 +33,10 @@ impl KeepaliveState {
|
|||
fn send_keepalive(
|
||||
mut clients: Query<(Entity, &mut Client, &mut KeepaliveState)>,
|
||||
server: Res<Server>,
|
||||
settings: Res<CoreSettings>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if server.current_tick() % (server.tps() * 10) == 0 {
|
||||
if server.current_tick() % (settings.tick_rate.get() * 10) as i64 == 0 {
|
||||
let mut rng = rand::thread_rng();
|
||||
let now = Instant::now();
|
||||
|
|
@ -1,6 +1,26 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![deny(
|
||||
rustdoc::broken_intra_doc_links,
|
||||
rustdoc::private_intra_doc_links,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
rustdoc::invalid_codeblock_attributes,
|
||||
rustdoc::invalid_rust_codeblocks,
|
||||
rustdoc::bare_urls,
|
||||
rustdoc::invalid_html_tags
|
||||
)]
|
||||
#![warn(
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unused_lifetimes,
|
||||
unused_import_braces,
|
||||
unreachable_pub,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::net::IpAddr;
|
||||
use std::num::Wrapping;
|
||||
use std::ops::Deref;
|
||||
use std::time::Instant;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
|
@ -11,15 +31,20 @@ use bytes::{Bytes, BytesMut};
|
|||
use glam::{DVec3, Vec3};
|
||||
use rand::Rng;
|
||||
use tracing::warn;
|
||||
use valence_protocol::block_pos::BlockPos;
|
||||
use valence_protocol::byte_angle::ByteAngle;
|
||||
use valence_protocol::encoder::PacketEncoder;
|
||||
use valence_protocol::ident::Ident;
|
||||
use valence_protocol::item::ItemStack;
|
||||
use valence_protocol::packet::s2c::play::game_state_change::GameEventKind;
|
||||
use valence_protocol::packet::s2c::play::particle::Particle;
|
||||
use valence_protocol::packet::s2c::play::player_position_look::Flags as PlayerPositionLookFlags;
|
||||
use valence_protocol::packet::s2c::play::{
|
||||
use uuid::Uuid;
|
||||
use valence_biome::BiomeRegistry;
|
||||
use valence_core::block_pos::BlockPos;
|
||||
use valence_core::chunk_pos::{ChunkPos, ChunkView};
|
||||
use valence_core::despawn::Despawned;
|
||||
use valence_core::game_mode::GameMode;
|
||||
use valence_core::ident::Ident;
|
||||
use valence_core::packet::byte_angle::ByteAngle;
|
||||
use valence_core::packet::encode::{PacketEncoder, WritePacket};
|
||||
use valence_core::packet::global_pos::GlobalPos;
|
||||
use valence_core::packet::s2c::play::game_state_change::GameEventKind;
|
||||
use valence_core::packet::s2c::play::particle::Particle;
|
||||
use valence_core::packet::s2c::play::player_position_look::Flags as PlayerPositionLookFlags;
|
||||
use valence_core::packet::s2c::play::{
|
||||
ChunkLoadDistanceS2c, ChunkRenderDistanceCenterS2c, CustomPayloadS2c, DeathMessageS2c,
|
||||
DisconnectS2c, EntitiesDestroyS2c, EntitySetHeadYawS2c, EntitySpawnS2c, EntityStatusS2c,
|
||||
EntityTrackerUpdateS2c, EntityVelocityUpdateS2c, ExperienceOrbSpawnS2c, GameJoinS2c,
|
||||
|
@ -27,46 +52,52 @@ use valence_protocol::packet::s2c::play::{
|
|||
PlayerActionResponseS2c, PlayerPositionLookS2c, PlayerRespawnS2c, PlayerSpawnPositionS2c,
|
||||
PlayerSpawnS2c, ResourcePackSendS2c, SubtitleS2c, TitleFadeS2c, TitleS2c, UnloadChunkS2c,
|
||||
};
|
||||
use valence_protocol::sound::Sound;
|
||||
use valence_protocol::text::Text;
|
||||
use valence_protocol::types::{GlobalPos, SoundCategory};
|
||||
use valence_protocol::var_int::VarInt;
|
||||
use valence_protocol::Packet;
|
||||
|
||||
use crate::biome::BiomeRegistry;
|
||||
use crate::component::{
|
||||
Despawned, GameMode, Location, Look, OldLocation, OldPosition, OnGround, Ping, Position,
|
||||
Properties, ScratchBuf, UniqueId, Username,
|
||||
use valence_core::packet::var_int::VarInt;
|
||||
use valence_core::packet::Packet;
|
||||
use valence_core::property::Property;
|
||||
use valence_core::scratch::ScratchBuf;
|
||||
use valence_core::sound::{Sound, SoundCategory};
|
||||
use valence_core::text::Text;
|
||||
use valence_core::uuid::UniqueId;
|
||||
use valence_core::Server;
|
||||
use valence_entity::player::PlayerEntityBundle;
|
||||
use valence_entity::{
|
||||
ClearEntityChangesSet, EntityId, EntityKind, EntityStatus, HeadYaw, Location, Look, ObjectData,
|
||||
OldLocation, OldPosition, OnGround, PacketByteRange, Position, TrackedData, Velocity,
|
||||
};
|
||||
use crate::entity::player::PlayerEntityBundle;
|
||||
use crate::entity::{
|
||||
EntityId, EntityKind, EntityStatus, HeadYaw, ObjectData, PacketByteRange, TrackedData, Velocity,
|
||||
};
|
||||
use crate::instance::{Instance, WriteUpdatePacketsToInstancesSet};
|
||||
use crate::inventory::{Inventory, InventoryKind};
|
||||
use crate::packet::WritePacket;
|
||||
use crate::registry_codec::{RegistryCodec, RegistryCodecSet};
|
||||
use crate::server::{NewClientInfo, Server};
|
||||
use crate::util::velocity_to_packet_units;
|
||||
use crate::view::{ChunkPos, ChunkView};
|
||||
use valence_instance::{ClearInstanceChangesSet, Instance, WriteUpdatePacketsToInstancesSet};
|
||||
use valence_registry::{RegistryCodec, RegistryCodecSet};
|
||||
|
||||
pub mod action;
|
||||
pub mod command;
|
||||
pub mod event_loop;
|
||||
pub mod interact_entity;
|
||||
pub mod keepalive;
|
||||
pub mod misc;
|
||||
pub mod movement;
|
||||
pub mod settings;
|
||||
pub mod teleport;
|
||||
pub mod weather;
|
||||
|
||||
pub(crate) struct ClientPlugin;
|
||||
pub struct ClientPlugin;
|
||||
|
||||
/// When clients have their packet buffer flushed. Any system that writes
|
||||
/// packets to clients should happen before this. Otherwise, the data
|
||||
/// packets to clients should happen _before_ this. Otherwise, the data
|
||||
/// will arrive one tick late.
|
||||
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct FlushPacketsSet;
|
||||
|
||||
/// The [`SystemSet`] in [`CoreSet::PreUpdate`] where new clients should be
|
||||
/// spawned. Systems that need to perform initialization work on clients before
|
||||
/// users get access to it should run _after_ this set in
|
||||
/// [`CoreSet::PreUpdate`].
|
||||
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
|
||||
pub struct SpawnClientsSet;
|
||||
|
||||
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
struct UpdateClientsSet;
|
||||
|
||||
impl Plugin for ClientPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
|
@ -86,16 +117,20 @@ impl Plugin for ClientPlugin {
|
|||
init_tracked_data.after(WriteUpdatePacketsToInstancesSet),
|
||||
update_op_level,
|
||||
)
|
||||
.in_set(UpdateClientsSet),
|
||||
)
|
||||
.configure_sets((
|
||||
SpawnClientsSet.in_base_set(CoreSet::PreUpdate),
|
||||
UpdateClientsSet
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.before(FlushPacketsSet),
|
||||
)
|
||||
.configure_set(
|
||||
FlushPacketsSet
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.after(WriteUpdatePacketsToInstancesSet),
|
||||
)
|
||||
ClearEntityChangesSet.after(UpdateClientsSet),
|
||||
FlushPacketsSet.in_base_set(CoreSet::PostUpdate),
|
||||
ClearInstanceChangesSet.after(FlushPacketsSet),
|
||||
))
|
||||
.add_system(flush_packets.in_set(FlushPacketsSet));
|
||||
|
||||
event_loop::build(app);
|
||||
movement::build(app);
|
||||
command::build(app);
|
||||
keepalive::build(app);
|
||||
|
@ -104,57 +139,54 @@ impl Plugin for ClientPlugin {
|
|||
misc::build(app);
|
||||
action::build(app);
|
||||
teleport::build(app);
|
||||
weather::build(app);
|
||||
}
|
||||
}
|
||||
|
||||
/// The bundle of components needed for clients to function. All components are
|
||||
/// required unless otherwise stated.
|
||||
#[derive(Bundle)]
|
||||
pub(crate) struct ClientBundle {
|
||||
client: Client,
|
||||
settings: settings::ClientSettings,
|
||||
scratch: ScratchBuf,
|
||||
entity_remove_buf: EntityRemoveBuf,
|
||||
username: Username,
|
||||
ip: Ip,
|
||||
properties: Properties,
|
||||
compass_pos: CompassPos,
|
||||
game_mode: GameMode,
|
||||
op_level: OpLevel,
|
||||
action_sequence: action::ActionSequence,
|
||||
view_distance: ViewDistance,
|
||||
old_view_distance: OldViewDistance,
|
||||
death_location: DeathLocation,
|
||||
keepalive_state: keepalive::KeepaliveState,
|
||||
ping: Ping,
|
||||
is_hardcore: IsHardcore,
|
||||
prev_game_mode: PrevGameMode,
|
||||
hashed_seed: HashedSeed,
|
||||
reduced_debug_info: ReducedDebugInfo,
|
||||
has_respawn_screen: HasRespawnScreen,
|
||||
is_debug: IsDebug,
|
||||
is_flat: IsFlat,
|
||||
teleport_state: teleport::TeleportState,
|
||||
cursor_item: CursorItem,
|
||||
player_inventory_state: ClientInventoryState,
|
||||
inventory: Inventory,
|
||||
player: PlayerEntityBundle,
|
||||
pub struct ClientBundle {
|
||||
pub client: Client,
|
||||
pub settings: settings::ClientSettings,
|
||||
pub scratch: ScratchBuf,
|
||||
pub entity_remove_buf: EntityRemoveBuf,
|
||||
pub username: Username,
|
||||
pub ip: Ip,
|
||||
pub properties: Properties,
|
||||
pub compass_pos: CompassPos,
|
||||
pub game_mode: GameMode,
|
||||
pub op_level: OpLevel,
|
||||
pub action_sequence: action::ActionSequence,
|
||||
pub view_distance: ViewDistance,
|
||||
pub old_view_distance: OldViewDistance,
|
||||
pub death_location: DeathLocation,
|
||||
pub keepalive_state: keepalive::KeepaliveState,
|
||||
pub ping: Ping,
|
||||
pub is_hardcore: IsHardcore,
|
||||
pub prev_game_mode: PrevGameMode,
|
||||
pub hashed_seed: HashedSeed,
|
||||
pub reduced_debug_info: ReducedDebugInfo,
|
||||
pub has_respawn_screen: HasRespawnScreen,
|
||||
pub is_debug: IsDebug,
|
||||
pub is_flat: IsFlat,
|
||||
pub teleport_state: teleport::TeleportState,
|
||||
pub player: PlayerEntityBundle,
|
||||
}
|
||||
|
||||
impl ClientBundle {
|
||||
pub(crate) fn new(
|
||||
info: NewClientInfo,
|
||||
conn: Box<dyn ClientConnection>,
|
||||
enc: PacketEncoder,
|
||||
) -> Self {
|
||||
pub fn new(args: ClientBundleArgs) -> Self {
|
||||
Self {
|
||||
client: Client { conn, enc },
|
||||
client: Client {
|
||||
conn: args.conn,
|
||||
enc: args.enc,
|
||||
},
|
||||
settings: settings::ClientSettings::default(),
|
||||
scratch: ScratchBuf::default(),
|
||||
entity_remove_buf: EntityRemoveBuf(vec![]),
|
||||
username: Username(info.username),
|
||||
ip: Ip(info.ip),
|
||||
properties: Properties(info.properties),
|
||||
username: Username(args.username),
|
||||
ip: Ip(args.ip),
|
||||
properties: Properties(args.properties),
|
||||
compass_pos: CompassPos::default(),
|
||||
game_mode: GameMode::default(),
|
||||
op_level: OpLevel::default(),
|
||||
|
@ -168,21 +200,30 @@ impl ClientBundle {
|
|||
is_hardcore: IsHardcore::default(),
|
||||
is_flat: IsFlat::default(),
|
||||
has_respawn_screen: HasRespawnScreen::default(),
|
||||
cursor_item: CursorItem::default(),
|
||||
player_inventory_state: ClientInventoryState::new(),
|
||||
inventory: Inventory::new(InventoryKind::Player),
|
||||
prev_game_mode: PrevGameMode::default(),
|
||||
hashed_seed: HashedSeed::default(),
|
||||
reduced_debug_info: ReducedDebugInfo::default(),
|
||||
is_debug: IsDebug::default(),
|
||||
player: PlayerEntityBundle {
|
||||
uuid: UniqueId(info.uuid),
|
||||
uuid: UniqueId(args.uuid),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Arguments for [`ClientBundle::new`].
|
||||
pub struct ClientBundleArgs {
|
||||
/// The username for the client.
|
||||
pub username: String,
|
||||
pub uuid: Uuid,
|
||||
pub ip: IpAddr,
|
||||
pub properties: Vec<Property>,
|
||||
pub conn: Box<dyn ClientConnection>,
|
||||
/// The packet encoder to use. This should be in sync with [`Self::conn`].
|
||||
pub enc: PacketEncoder,
|
||||
}
|
||||
|
||||
/// The main client component. Contains the underlying network connection and
|
||||
/// packet buffer.
|
||||
///
|
||||
|
@ -206,6 +247,7 @@ pub trait ClientConnection: Send + Sync + 'static {
|
|||
/// The number of pending packets waiting to be received via
|
||||
/// [`Self::try_recv`].
|
||||
fn len(&self) -> usize;
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
@ -380,8 +422,8 @@ impl Client {
|
|||
self.write_packet(&ParticleS2c {
|
||||
particle: Cow::Borrowed(particle),
|
||||
long_distance,
|
||||
position: position.into().into(),
|
||||
offset: offset.into().into(),
|
||||
position: position.into(),
|
||||
offset: offset.into(),
|
||||
max_speed,
|
||||
count,
|
||||
})
|
||||
|
@ -406,7 +448,7 @@ impl Client {
|
|||
self.write_packet(&PlaySoundS2c {
|
||||
id: sound.to_id(),
|
||||
category,
|
||||
position: (position * 8.0).as_ivec3().into(),
|
||||
position: (position * 8.0).as_ivec3(),
|
||||
volume,
|
||||
pitch,
|
||||
seed: rand::random(),
|
||||
|
@ -417,7 +459,7 @@ impl Client {
|
|||
pub fn set_velocity(&mut self, velocity: impl Into<Vec3>) {
|
||||
self.write_packet(&EntityVelocityUpdateS2c {
|
||||
entity_id: VarInt(0),
|
||||
velocity: velocity_to_packet_units(velocity.into()),
|
||||
velocity: Velocity(velocity.into()).to_packet_units(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -476,6 +518,74 @@ impl EntityRemoveBuf {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, PartialEq, Eq, Default, Debug)]
|
||||
pub struct Username(pub String);
|
||||
|
||||
impl Username {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
is_valid_username(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Username {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether or not the given string is a valid Minecraft username.
|
||||
///
|
||||
/// A valid username is 3 to 16 characters long with only ASCII alphanumeric
|
||||
/// characters. The username must match the regex `^[a-zA-Z0-9_]{3,16}$` to be
|
||||
/// considered valid.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use valence_client::is_valid_username;
|
||||
///
|
||||
/// assert!(is_valid_username("00a"));
|
||||
/// assert!(is_valid_username("jeb_"));
|
||||
///
|
||||
/// assert!(!is_valid_username("notavalidusername"));
|
||||
/// assert!(!is_valid_username("NotValid!"));
|
||||
/// ```
|
||||
pub fn is_valid_username(username: &str) -> bool {
|
||||
(3..=16).contains(&username.len())
|
||||
&& username
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, PartialEq, Eq, Default, Debug)]
|
||||
pub struct Properties(pub Vec<Property>);
|
||||
|
||||
impl Properties {
|
||||
/// Finds the property with the name "textures".
|
||||
pub fn textures(&self) -> Option<&Property> {
|
||||
self.0.iter().find(|prop| prop.name == "textures")
|
||||
}
|
||||
|
||||
/// Finds the property with the name "textures".
|
||||
pub fn textures_mut(&mut self) -> Option<&mut Property> {
|
||||
self.0.iter_mut().find(|prop| prop.name == "textures")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Property>> for Properties {
|
||||
fn from(value: Vec<Property>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Properties {
|
||||
type Target = [Property];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Ip(pub IpAddr);
|
||||
|
||||
|
@ -568,6 +678,16 @@ impl OldViewItem<'_> {
|
|||
#[derive(Component, Clone, PartialEq, Eq, Default, Debug)]
|
||||
pub struct DeathLocation(pub Option<(Ident<String>, BlockPos)>);
|
||||
|
||||
/// Delay measured in milliseconds. Negative values indicate absence.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct Ping(pub i32);
|
||||
|
||||
impl Default for Ping {
|
||||
fn default() -> Self {
|
||||
Self(-1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
|
||||
pub struct IsHardcore(pub bool);
|
||||
|
||||
|
@ -598,46 +718,6 @@ pub struct IsDebug(pub bool);
|
|||
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
|
||||
pub struct IsFlat(pub bool);
|
||||
|
||||
/// The item stack that the client thinks it's holding under the mouse
|
||||
/// cursor.
|
||||
#[derive(Component, Clone, PartialEq, Default, Debug)]
|
||||
pub struct CursorItem(pub Option<ItemStack>);
|
||||
|
||||
// TODO: move this component to inventory module?
|
||||
/// Miscellaneous inventory data.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct ClientInventoryState {
|
||||
/// The current window ID. Incremented when inventories are opened.
|
||||
pub(crate) window_id: u8,
|
||||
pub(crate) state_id: Wrapping<i32>,
|
||||
/// Tracks what slots have been changed by this client in this tick, so we
|
||||
/// don't need to send updates for them.
|
||||
pub(crate) slots_changed: u64,
|
||||
/// Whether the client has updated the cursor item in this tick. This is not
|
||||
/// on the `CursorItem` component to make maintaining accurate change
|
||||
/// detection for end users easier.
|
||||
pub(crate) client_updated_cursor_item: bool,
|
||||
// TODO: make this a separate modifiable component.
|
||||
pub(crate) held_item_slot: u16,
|
||||
}
|
||||
|
||||
impl ClientInventoryState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
window_id: 0,
|
||||
state_id: Wrapping(0),
|
||||
slots_changed: 0,
|
||||
client_updated_cursor_item: false,
|
||||
// First slot of the hotbar.
|
||||
held_item_slot: 36,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn held_item_slot(&self) -> u16 {
|
||||
self.held_item_slot
|
||||
}
|
||||
}
|
||||
|
||||
/// A system for adding [`Despawned`] components to disconnected clients. This
|
||||
/// works by listening for removed [`Client`] components.
|
||||
pub fn despawn_disconnected_clients(
|
||||
|
@ -701,7 +781,7 @@ fn initial_join(
|
|||
_ = q.client.enc.prepend_packet(&GameJoinS2c {
|
||||
entity_id: 0, // We reserve ID 0 for clients.
|
||||
is_hardcore: q.is_hardcore.0,
|
||||
game_mode: (*q.game_mode).into(),
|
||||
game_mode: *q.game_mode,
|
||||
previous_game_mode: q.prev_game_mode.0.map(|g| g as i8).unwrap_or(-1),
|
||||
dimension_names,
|
||||
registry_codec: Cow::Borrowed(codec.cached_codec()),
|
||||
|
@ -727,6 +807,7 @@ fn initial_join(
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn respawn(
|
||||
mut clients: Query<
|
||||
(
|
||||
|
@ -767,7 +848,7 @@ fn respawn(
|
|||
dimension_type_name: dimension_name.into(),
|
||||
dimension_name: dimension_name.into(),
|
||||
hashed_seed: hashed_seed.0,
|
||||
game_mode: (*game_mode).into(),
|
||||
game_mode: *game_mode,
|
||||
previous_game_mode: prev_game_mode.0.map(|g| g as i8).unwrap_or(-1),
|
||||
is_debug: is_debug.0,
|
||||
is_flat: is_flat.0,
|
||||
|
@ -795,28 +876,28 @@ fn update_chunk_load_dist(
|
|||
}
|
||||
|
||||
#[derive(WorldQuery)]
|
||||
pub(crate) struct EntityInitQuery {
|
||||
pub entity_id: &'static EntityId,
|
||||
pub uuid: &'static UniqueId,
|
||||
pub kind: &'static EntityKind,
|
||||
pub look: &'static Look,
|
||||
pub head_yaw: &'static HeadYaw,
|
||||
pub on_ground: &'static OnGround,
|
||||
pub object_data: &'static ObjectData,
|
||||
pub velocity: &'static Velocity,
|
||||
pub tracked_data: &'static TrackedData,
|
||||
struct EntityInitQuery {
|
||||
entity_id: &'static EntityId,
|
||||
uuid: &'static UniqueId,
|
||||
kind: &'static EntityKind,
|
||||
look: &'static Look,
|
||||
head_yaw: &'static HeadYaw,
|
||||
on_ground: &'static OnGround,
|
||||
object_data: &'static ObjectData,
|
||||
velocity: &'static Velocity,
|
||||
tracked_data: &'static TrackedData,
|
||||
}
|
||||
|
||||
impl EntityInitQueryItem<'_> {
|
||||
/// Writes the appropriate packets to initialize an entity. This will spawn
|
||||
/// the entity and initialize tracked data.
|
||||
pub fn write_init_packets(&self, pos: DVec3, mut writer: impl WritePacket) {
|
||||
fn write_init_packets(&self, pos: DVec3, mut writer: impl WritePacket) {
|
||||
match *self.kind {
|
||||
EntityKind::MARKER => {}
|
||||
EntityKind::EXPERIENCE_ORB => {
|
||||
writer.write_packet(&ExperienceOrbSpawnS2c {
|
||||
entity_id: self.entity_id.get().into(),
|
||||
position: pos.to_array(),
|
||||
position: pos,
|
||||
count: self.object_data.0 as i16,
|
||||
});
|
||||
}
|
||||
|
@ -824,7 +905,7 @@ impl EntityInitQueryItem<'_> {
|
|||
writer.write_packet(&PlayerSpawnS2c {
|
||||
entity_id: self.entity_id.get().into(),
|
||||
player_uuid: self.uuid.0,
|
||||
position: pos.to_array(),
|
||||
position: pos,
|
||||
yaw: ByteAngle::from_degrees(self.look.yaw),
|
||||
pitch: ByteAngle::from_degrees(self.look.pitch),
|
||||
});
|
||||
|
@ -839,12 +920,12 @@ impl EntityInitQueryItem<'_> {
|
|||
entity_id: self.entity_id.get().into(),
|
||||
object_uuid: self.uuid.0,
|
||||
kind: self.kind.get().into(),
|
||||
position: pos.to_array(),
|
||||
position: pos,
|
||||
pitch: ByteAngle::from_degrees(self.look.pitch),
|
||||
yaw: ByteAngle::from_degrees(self.look.yaw),
|
||||
head_yaw: ByteAngle::from_degrees(self.head_yaw.0),
|
||||
data: self.object_data.0.into(),
|
||||
velocity: velocity_to_packet_units(self.velocity.0),
|
||||
velocity: self.velocity.to_packet_units(),
|
||||
}),
|
||||
}
|
||||
|
||||
|
@ -857,6 +938,7 @@ impl EntityInitQueryItem<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn read_data_in_old_view(
|
||||
mut clients: Query<(
|
||||
&mut Client,
|
||||
|
@ -892,10 +974,7 @@ fn read_data_in_old_view(
|
|||
if let Some(cell) = instance.partition.get(&pos) {
|
||||
if cell.chunk_removed && cell.chunk.is_none() {
|
||||
// Chunk was previously loaded and is now deleted.
|
||||
client.write_packet(&UnloadChunkS2c {
|
||||
chunk_x: pos.x,
|
||||
chunk_z: pos.z,
|
||||
});
|
||||
client.write_packet(&UnloadChunkS2c { pos });
|
||||
}
|
||||
|
||||
if let Some(chunk) = &cell.chunk {
|
||||
|
@ -952,6 +1031,7 @@ fn read_data_in_old_view(
|
|||
///
|
||||
/// This handles the situation when a client changes instances or chunk
|
||||
/// position. It must run after [`read_data_in_old_view`].
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn update_view(
|
||||
mut clients: Query<
|
||||
(
|
||||
|
@ -1009,10 +1089,7 @@ fn update_view(
|
|||
if let Some(cell) = old_instance.partition.get(&pos) {
|
||||
// Unload the chunk at this cell if it was loaded.
|
||||
if cell.chunk.is_some() {
|
||||
client.write_packet(&UnloadChunkS2c {
|
||||
chunk_x: pos.x,
|
||||
chunk_z: pos.z,
|
||||
});
|
||||
client.write_packet(&UnloadChunkS2c { pos });
|
||||
}
|
||||
|
||||
// Unload all the entities in the cell.
|
||||
|
@ -1069,10 +1146,7 @@ fn update_view(
|
|||
if let Some(cell) = instance.partition.get(&pos) {
|
||||
// Unload the chunk at this cell if it was loaded.
|
||||
if cell.chunk.is_some() {
|
||||
client.write_packet(&UnloadChunkS2c {
|
||||
chunk_x: pos.x,
|
||||
chunk_z: pos.z,
|
||||
});
|
||||
client.write_packet(&UnloadChunkS2c { pos });
|
||||
}
|
||||
|
||||
// Unload all the entities in the cell.
|
||||
|
@ -1204,102 +1278,3 @@ fn update_op_level(mut clients: Query<(&mut Client, &OpLevel), Changed<OpLevel>>
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use bevy_app::App;
|
||||
use bevy_ecs::world::EntityMut;
|
||||
use valence_protocol::packet::s2c::play::ChunkDataS2c;
|
||||
use valence_protocol::packet::S2cPlayPacket;
|
||||
|
||||
use super::*;
|
||||
use crate::instance::Chunk;
|
||||
use crate::unit_test::util::scenario_single_client;
|
||||
|
||||
#[test]
|
||||
fn client_chunk_view_change() {
|
||||
let mut app = App::new();
|
||||
|
||||
let (client_ent, mut client_helper) = scenario_single_client(&mut app);
|
||||
|
||||
let mut instance = app
|
||||
.world
|
||||
.query::<&mut Instance>()
|
||||
.single_mut(&mut app.world);
|
||||
|
||||
for z in -15..15 {
|
||||
for x in -15..15 {
|
||||
instance.insert_chunk([x, z], Chunk::default());
|
||||
}
|
||||
}
|
||||
|
||||
let mut client = app.world.entity_mut(client_ent);
|
||||
|
||||
client.get_mut::<Position>().unwrap().set([8.0, 0.0, 8.0]);
|
||||
client.get_mut::<ViewDistance>().unwrap().0 = 6;
|
||||
|
||||
// Tick
|
||||
app.update();
|
||||
let mut client = app.world.entity_mut(client_ent);
|
||||
|
||||
let mut loaded_chunks = BTreeSet::new();
|
||||
|
||||
for pkt in client_helper.collect_sent() {
|
||||
if let S2cPlayPacket::ChunkDataS2c(ChunkDataS2c {
|
||||
chunk_x, chunk_z, ..
|
||||
}) = pkt
|
||||
{
|
||||
assert!(
|
||||
loaded_chunks.insert(ChunkPos::new(chunk_x, chunk_z)),
|
||||
"({chunk_x}, {chunk_z})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for pos in view(&client).iter() {
|
||||
assert!(loaded_chunks.contains(&pos), "{pos:?}");
|
||||
}
|
||||
|
||||
assert!(!loaded_chunks.is_empty());
|
||||
|
||||
// Move the client to the adjacent chunk.
|
||||
client.get_mut::<Position>().unwrap().set([24.0, 0.0, 24.0]);
|
||||
|
||||
// Tick
|
||||
app.update();
|
||||
let client = app.world.entity_mut(client_ent);
|
||||
|
||||
for pkt in client_helper.collect_sent() {
|
||||
match pkt {
|
||||
S2cPlayPacket::ChunkDataS2c(ChunkDataS2c {
|
||||
chunk_x, chunk_z, ..
|
||||
}) => {
|
||||
assert!(
|
||||
loaded_chunks.insert(ChunkPos::new(chunk_x, chunk_z)),
|
||||
"({chunk_x}, {chunk_z})"
|
||||
);
|
||||
}
|
||||
S2cPlayPacket::UnloadChunkS2c(UnloadChunkS2c { chunk_x, chunk_z }) => {
|
||||
assert!(
|
||||
loaded_chunks.remove(&ChunkPos::new(chunk_x, chunk_z)),
|
||||
"({chunk_x}, {chunk_z})"
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for pos in view(&client).iter() {
|
||||
assert!(loaded_chunks.contains(&pos), "{pos:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn view(client: &EntityMut) -> ChunkView {
|
||||
let chunk_pos = client.get::<Position>().unwrap().chunk_pos();
|
||||
let view_dist = client.get::<ViewDistance>().unwrap().0;
|
||||
|
||||
ChunkView::new(chunk_pos, view_dist)
|
||||
}
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use glam::Vec3;
|
||||
use valence_protocol::block_pos::BlockPos;
|
||||
use valence_protocol::packet::c2s::play::{
|
||||
use valence_core::block_pos::BlockPos;
|
||||
use valence_core::direction::Direction;
|
||||
use valence_core::hand::Hand;
|
||||
use valence_core::packet::c2s::play::{
|
||||
ChatMessageC2s, ClientStatusC2s, HandSwingC2s, PlayerInteractBlockC2s, PlayerInteractItemC2s,
|
||||
ResourcePackStatusC2s,
|
||||
};
|
||||
use valence_protocol::types::{Direction, Hand};
|
||||
use valence_entity::{EntityAnimation, EntityAnimations};
|
||||
|
||||
use super::action::ActionSequence;
|
||||
use crate::entity::{EntityAnimation, EntityAnimations};
|
||||
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
|
||||
|
||||
pub(super) fn build(app: &mut App) {
|
||||
|
@ -118,7 +119,7 @@ fn handle_misc_packets(
|
|||
hand: pkt.hand,
|
||||
position: pkt.position,
|
||||
face: pkt.face,
|
||||
cursor_pos: pkt.cursor_pos.into(),
|
||||
cursor_pos: pkt.cursor_pos,
|
||||
head_inside_block: pkt.head_inside_block,
|
||||
sequence: pkt.sequence.0,
|
||||
});
|
|
@ -1,13 +1,12 @@
|
|||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use glam::DVec3;
|
||||
use valence_protocol::packet::c2s::play::{
|
||||
use valence_core::packet::c2s::play::{
|
||||
Full, LookAndOnGround, OnGroundOnly, PositionAndOnGround, VehicleMoveC2s,
|
||||
};
|
||||
use valence_entity::{HeadYaw, Look, OnGround, Position};
|
||||
|
||||
use super::teleport::TeleportState;
|
||||
use crate::component::{Look, OnGround, Position};
|
||||
use crate::entity::HeadYaw;
|
||||
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
|
||||
|
||||
pub(super) fn build(app: &mut App) {
|
||||
|
@ -56,7 +55,7 @@ fn handle_client_movement(
|
|||
{
|
||||
let mov = Movement {
|
||||
client: packet.client,
|
||||
position: pkt.position.into(),
|
||||
position: pkt.position,
|
||||
old_position: pos.0,
|
||||
look: *look,
|
||||
old_look: *look,
|
||||
|
@ -80,7 +79,7 @@ fn handle_client_movement(
|
|||
{
|
||||
let mov = Movement {
|
||||
client: packet.client,
|
||||
position: pkt.position.into(),
|
||||
position: pkt.position,
|
||||
old_position: pos.0,
|
||||
look: Look {
|
||||
yaw: pkt.yaw,
|
||||
|
@ -158,7 +157,7 @@ fn handle_client_movement(
|
|||
{
|
||||
let mov = Movement {
|
||||
client: packet.client,
|
||||
position: pkt.position.into(),
|
||||
position: pkt.position,
|
||||
old_position: pos.0,
|
||||
look: Look {
|
||||
yaw: pkt.yaw,
|
|
@ -1,9 +1,8 @@
|
|||
pub use valence_protocol::packet::c2s::play::client_settings::ChatMode;
|
||||
// use valence_protocol::packet::c2s::play::client_settings::MainArm;
|
||||
use valence_protocol::packet::c2s::play::ClientSettingsC2s;
|
||||
pub use valence_core::packet::c2s::play::client_settings::ChatMode;
|
||||
use valence_core::packet::c2s::play::ClientSettingsC2s;
|
||||
pub use valence_entity::player::{MainArm, PlayerModelParts};
|
||||
|
||||
use super::*;
|
||||
pub use crate::entity::player::{MainArm, PlayerModelParts};
|
||||
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
|
||||
|
||||
pub(super) fn build(app: &mut App) {
|
|
@ -1,15 +1,10 @@
|
|||
use valence_protocol::packet::c2s::play::TeleportConfirmC2s;
|
||||
use valence_core::packet::c2s::play::TeleportConfirmC2s;
|
||||
|
||||
use super::*;
|
||||
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
|
||||
|
||||
pub(super) fn build(app: &mut App) {
|
||||
app.add_system(
|
||||
teleport
|
||||
.after(update_view)
|
||||
.before(FlushPacketsSet)
|
||||
.in_base_set(CoreSet::PostUpdate),
|
||||
)
|
||||
app.add_system(teleport.after(update_view).in_set(UpdateClientsSet))
|
||||
.add_system(
|
||||
handle_teleport_confirmations
|
||||
.in_schedule(EventLoopSchedule)
|
||||
|
@ -56,6 +51,7 @@ impl TeleportState {
|
|||
///
|
||||
/// This should happen after chunks are loaded so the client doesn't fall though
|
||||
/// the floor.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn teleport(
|
||||
mut clients: Query<
|
||||
(&mut Client, &mut TeleportState, &Position, &Look),
|
||||
|
@ -79,7 +75,7 @@ fn teleport(
|
|||
.with_x_rot(!changed_pitch);
|
||||
|
||||
client.write_packet(&PlayerPositionLookS2c {
|
||||
position: if changed_pos { pos.0.into() } else { [0.0; 3] },
|
||||
position: if changed_pos { pos.0 } else { DVec3::ZERO },
|
||||
yaw: if changed_yaw { look.yaw } else { 0.0 },
|
||||
pitch: if changed_pitch { look.pitch } else { 0.0 },
|
||||
flags,
|
226
crates/valence_client/src/weather.rs
Normal file
226
crates/valence_client/src/weather.rs
Normal file
|
@ -0,0 +1,226 @@
|
|||
//! The weather system.
|
||||
//!
|
||||
//! This module contains the systems and components needed to handle
|
||||
//! weather.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! The components may be attached to clients or instances.
|
||||
//!
|
||||
//! - [`Rain`]: When attached, raining begin and rain level set events are
|
||||
//! emitted. When removed, the end raining event is emitted.
|
||||
//! - [`Thunder`]: When attached, thunder level set event is emitted. When
|
||||
//! removed, the thunder level set to zero event is emitted.
|
||||
//!
|
||||
//! New joined players are handled, so that they are get weather events from
|
||||
//! the instance.
|
||||
|
||||
use valence_core::packet::s2c::play::game_state_change::GameEventKind;
|
||||
use valence_core::packet::s2c::play::GameStateChangeS2c;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
struct UpdateWeatherPerInstanceSet;
|
||||
|
||||
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
struct UpdateWeatherPerClientSet;
|
||||
|
||||
pub(super) fn build(app: &mut App) {
|
||||
app.configure_set(
|
||||
UpdateWeatherPerInstanceSet
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.before(WriteUpdatePacketsToInstancesSet),
|
||||
)
|
||||
.configure_set(
|
||||
UpdateWeatherPerClientSet
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.before(FlushPacketsSet),
|
||||
)
|
||||
.add_systems(
|
||||
(
|
||||
handle_rain_begin_per_instance,
|
||||
handle_rain_change_per_instance,
|
||||
handle_rain_end_per_instance,
|
||||
handle_thunder_change_per_instance,
|
||||
handle_thunder_end_per_instance,
|
||||
)
|
||||
.chain()
|
||||
.in_set(UpdateWeatherPerInstanceSet)
|
||||
.before(UpdateWeatherPerClientSet),
|
||||
)
|
||||
.add_systems(
|
||||
(
|
||||
handle_rain_begin_per_client,
|
||||
handle_rain_change_per_client,
|
||||
handle_rain_end_per_client,
|
||||
handle_thunder_change_per_client,
|
||||
handle_thunder_end_per_client,
|
||||
)
|
||||
.chain()
|
||||
.in_set(UpdateWeatherPerClientSet),
|
||||
)
|
||||
.add_system(
|
||||
handle_weather_for_joined_player
|
||||
.before(UpdateWeatherPerClientSet)
|
||||
.in_base_set(CoreSet::PostUpdate),
|
||||
);
|
||||
}
|
||||
|
||||
/// Contains the rain level.
|
||||
///
|
||||
/// Valid values are within `0.0..=1.0`.
|
||||
#[derive(Component)]
|
||||
pub struct Rain(pub f32);
|
||||
|
||||
/// Contains the thunder level.
|
||||
///
|
||||
/// Valid values are within `0.0..=1.0`.
|
||||
#[derive(Component)]
|
||||
pub struct Thunder(pub f32);
|
||||
|
||||
fn handle_weather_for_joined_player(
|
||||
mut clients: Query<(&mut Client, &Location), Added<Client>>,
|
||||
weathers: Query<(Option<&Rain>, Option<&Thunder>), With<Instance>>,
|
||||
) {
|
||||
for (mut client, loc) in &mut clients {
|
||||
if let Ok((rain, thunder)) = weathers.get(loc.0) {
|
||||
if let Some(level) = rain {
|
||||
client.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::BeginRaining,
|
||||
value: 0.0,
|
||||
});
|
||||
|
||||
client.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::RainLevelChange,
|
||||
value: level.0,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(level) = thunder {
|
||||
client.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::ThunderLevelChange,
|
||||
value: level.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rain_begin_per_instance(mut instances: Query<&mut Instance, Added<Rain>>) {
|
||||
for mut instance in &mut instances {
|
||||
instance.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::BeginRaining,
|
||||
value: f32::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rain_change_per_instance(mut instances: Query<(&mut Instance, &Rain), Changed<Rain>>) {
|
||||
for (mut instance, rain) in &mut instances {
|
||||
instance.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::RainLevelChange,
|
||||
value: rain.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rain_end_per_instance(
|
||||
mut instances: Query<&mut Instance>,
|
||||
mut removed: RemovedComponents<Rain>,
|
||||
) {
|
||||
for entity in &mut removed {
|
||||
if let Ok(mut instance) = instances.get_mut(entity) {
|
||||
instance.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::EndRaining,
|
||||
value: 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_thunder_change_per_instance(
|
||||
mut instances: Query<(&mut Instance, &Thunder), Changed<Thunder>>,
|
||||
) {
|
||||
for (mut instance, thunder) in &mut instances {
|
||||
instance.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::ThunderLevelChange,
|
||||
value: thunder.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_thunder_end_per_instance(
|
||||
mut instances: Query<&mut Instance>,
|
||||
mut removed: RemovedComponents<Thunder>,
|
||||
) {
|
||||
for entity in &mut removed {
|
||||
if let Ok(mut instance) = instances.get_mut(entity) {
|
||||
instance.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::ThunderLevelChange,
|
||||
value: 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rain_begin_per_client(mut clients: Query<&mut Client, (Added<Rain>, Without<Instance>)>) {
|
||||
for mut client in &mut clients {
|
||||
client.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::BeginRaining,
|
||||
value: 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn handle_rain_change_per_client(
|
||||
mut clients: Query<(&mut Client, &Rain), (Changed<Rain>, Without<Instance>)>,
|
||||
) {
|
||||
for (mut client, rain) in &mut clients {
|
||||
client.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::RainLevelChange,
|
||||
value: rain.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rain_end_per_client(
|
||||
mut clients: Query<&mut Client>,
|
||||
mut removed: RemovedComponents<Rain>,
|
||||
) {
|
||||
for entity in &mut removed {
|
||||
if let Ok(mut client) = clients.get_mut(entity) {
|
||||
client.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::EndRaining,
|
||||
value: f32::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn handle_thunder_change_per_client(
|
||||
mut clients: Query<(&mut Client, &Thunder), (Changed<Thunder>, Without<Instance>)>,
|
||||
) {
|
||||
for (mut client, thunder) in &mut clients {
|
||||
client.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::ThunderLevelChange,
|
||||
value: thunder.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_thunder_end_per_client(
|
||||
mut clients: Query<&mut Client, Without<Instance>>,
|
||||
mut removed: RemovedComponents<Thunder>,
|
||||
) {
|
||||
for entity in &mut removed {
|
||||
if let Ok(mut client) = clients.get_mut(entity) {
|
||||
client.write_packet(&GameStateChangeS2c {
|
||||
kind: GameEventKind::ThunderLevelChange,
|
||||
value: 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
44
crates/valence_core/Cargo.toml
Normal file
44
crates/valence_core/Cargo.toml
Normal file
|
@ -0,0 +1,44 @@
|
|||
[package]
|
||||
name = "valence_core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
build = "build/main.rs"
|
||||
|
||||
[features]
|
||||
encryption = ["dep:aes", "dep:cfb8"]
|
||||
compression = ["dep:flate2"]
|
||||
|
||||
[dependencies]
|
||||
aes = { workspace = true, optional = true }
|
||||
anyhow.workspace = true
|
||||
bevy_app.workspace = true
|
||||
bevy_ecs.workspace = true
|
||||
bitfield-struct.workspace = true
|
||||
byteorder.workspace = true
|
||||
bytes.workspace = true
|
||||
cfb8 = { workspace = true, optional = true }
|
||||
flate2 = { workspace = true, optional = true }
|
||||
glam.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
valence_nbt.workspace = true
|
||||
valence_core_macros.workspace = true
|
||||
url.workspace = true
|
||||
base64.workspace = true
|
||||
rand.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rand.workspace = true
|
||||
valence_core = { workspace = true, features = ["compression"] }
|
||||
|
||||
[build-dependencies]
|
||||
anyhow.workspace = true
|
||||
heck.workspace = true
|
||||
proc-macro2.workspace = true
|
||||
quote.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
valence_build_utils.workspace = true
|
3
crates/valence_core/README.md
Normal file
3
crates/valence_core/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# valence_core
|
||||
|
||||
Contains foundational code or modules too small to deserve their own crate. The contents of `valence_core` are re-exported by the main `valence` crate.
|
|
@ -3,8 +3,7 @@ use heck::ToPascalCase;
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ident;
|
||||
use valence_build_utils::ident;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
struct Item {
|
||||
|
@ -280,6 +279,7 @@ pub fn build() -> anyhow::Result<TokenStream> {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// Constructs an item kind from a block kind.
|
||||
///
|
||||
/// [`ItemKind::Air`] is used to indicate the absence of an item.
|
||||
|
@ -292,7 +292,7 @@ pub fn build() -> anyhow::Result<TokenStream> {
|
|||
/// If the given item kind doesn't have a corresponding block kind, `None` is returned.
|
||||
pub const fn to_block_kind(self) -> Option<BlockKind> {
|
||||
BlockKind::from_item_kind(self)
|
||||
}
|
||||
}*/
|
||||
|
||||
/// An array of all item kinds.
|
||||
pub const ALL: [Self; #item_kind_count] = [#(Self::#item_kind_variants,)*];
|
22
crates/valence_core/build/main.rs
Normal file
22
crates/valence_core/build/main.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use valence_build_utils::{rerun_if_changed, write_generated_file};
|
||||
|
||||
mod item;
|
||||
mod packet_id;
|
||||
mod sound;
|
||||
mod translation_key;
|
||||
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
rerun_if_changed([
|
||||
"../../extracted/items.json",
|
||||
"../../extracted/packets.json",
|
||||
"../../extracted/sounds.json",
|
||||
"../../extracted/translation_keys.json",
|
||||
]);
|
||||
|
||||
write_generated_file(item::build()?, "item.rs")?;
|
||||
write_generated_file(sound::build()?, "sound.rs")?;
|
||||
write_generated_file(translation_key::build()?, "translation_key.rs")?;
|
||||
write_generated_file(packet_id::build()?, "packet_id.rs")?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -2,8 +2,7 @@ use heck::ToPascalCase;
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ident;
|
||||
use valence_build_utils::ident;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Packet {
|
||||
|
@ -30,7 +29,7 @@ pub fn build() -> anyhow::Result<TokenStream> {
|
|||
consts.extend([quote! {
|
||||
#[doc = #doc]
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const #name_ident: i32 = #id;
|
||||
pub(crate) const #name_ident: i32 = #id;
|
||||
}]);
|
||||
}
|
||||
|
|
@ -2,8 +2,7 @@ use heck::ToPascalCase;
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ident;
|
||||
use valence_build_utils::ident;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Sound {
|
|
@ -3,8 +3,7 @@ use heck::ToShoutySnakeCase;
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ident;
|
||||
use valence_build_utils::ident;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
struct Translation {
|
||||
|
@ -28,7 +27,7 @@ pub fn build() -> anyhow::Result<TokenStream> {
|
|||
let const_id = ident(translation.key.to_shouty_snake_case());
|
||||
let key = &translation.key;
|
||||
let english_translation = &translation.english_translation;
|
||||
let doc = escape(english_translation);
|
||||
let doc = format!("\"{}\"", escape(english_translation));
|
||||
|
||||
quote! {
|
||||
#[doc = #doc]
|
38
crates/valence_core/src/aabb.rs
Normal file
38
crates/valence_core/src/aabb.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use glam::DVec3;
|
||||
|
||||
/// 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 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@ use std::io::Write;
|
|||
|
||||
use anyhow::bail;
|
||||
|
||||
use crate::types::Direction;
|
||||
use crate::{Decode, Encode};
|
||||
use crate::direction::Direction;
|
||||
use crate::packet::{Decode, Encode};
|
||||
|
||||
/// Represents an absolute block position in world space.
|
||||
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)]
|
||||
|
@ -28,8 +28,8 @@ impl BlockPos {
|
|||
/// direction.
|
||||
///
|
||||
/// ```
|
||||
/// use valence_protocol::block_pos::BlockPos;
|
||||
/// use valence_protocol::types::Direction;
|
||||
/// use valence_core::block_pos::BlockPos;
|
||||
/// use valence_core::direction::Direction;
|
||||
///
|
||||
/// let pos = BlockPos::new(0, 0, 0);
|
||||
/// let adj = pos.get_in_direction(Direction::South);
|
|
@ -1,9 +1,10 @@
|
|||
use glam::DVec3;
|
||||
use valence_protocol::block_pos::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)]
|
||||
use crate::block_pos::BlockPos;
|
||||
use crate::packet::{Decode, Encode};
|
||||
|
||||
/// The X and Z position of a chunk.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Debug, Encode, Decode)]
|
||||
pub struct ChunkPos {
|
||||
/// The X position of the chunk.
|
||||
pub x: i32,
|
||||
|
@ -120,7 +121,7 @@ impl ChunkView {
|
|||
// The foreach-based methods are optimizing better than the iterator ones.
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn for_each(self, mut f: impl FnMut(ChunkPos)) {
|
||||
pub 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 {
|
||||
|
@ -134,7 +135,7 @@ impl ChunkView {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn diff_for_each(self, other: Self, mut f: impl FnMut(ChunkPos)) {
|
||||
pub fn diff_for_each(self, other: Self, mut f: impl FnMut(ChunkPos)) {
|
||||
self.for_each(|p| {
|
||||
if !other.contains(p) {
|
||||
f(p);
|
26
crates/valence_core/src/despawn.rs
Normal file
26
crates/valence_core/src/despawn.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use bevy_ecs::prelude::*;
|
||||
|
||||
/// A marker [`Component`] for entities that should be despawned at the end of
|
||||
/// the tick.
|
||||
///
|
||||
/// In Valence, some entities such as Minecraft entities must not be removed
|
||||
/// from the [`World`] directly. Valence needs an opportunity to perform
|
||||
/// deinitialization work while the entity's components still exist.
|
||||
///
|
||||
/// To resolve this problem, 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.
|
||||
///
|
||||
/// The `Despawned` component can be used on entities that Valence does not know
|
||||
/// about. The entity will be despawned regardless.
|
||||
#[derive(Component, Copy, Clone, Default, PartialEq, Eq, Debug)]
|
||||
pub struct Despawned;
|
||||
|
||||
pub(super) fn despawn_marked_entities(
|
||||
mut commands: Commands,
|
||||
entities: Query<Entity, With<Despawned>>,
|
||||
) {
|
||||
for entity in &entities {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
9
crates/valence_core/src/difficulty.rs
Normal file
9
crates/valence_core/src/difficulty.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use crate::packet::{Decode, Encode};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
||||
pub enum Difficulty {
|
||||
Peaceful,
|
||||
Easy,
|
||||
Normal,
|
||||
Hard,
|
||||
}
|
19
crates/valence_core/src/direction.rs
Normal file
19
crates/valence_core/src/direction.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use bevy_ecs::prelude::*;
|
||||
|
||||
use crate::packet::{Decode, Encode};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode, Component)]
|
||||
pub enum Direction {
|
||||
/// -Y
|
||||
Down,
|
||||
/// +Y
|
||||
Up,
|
||||
/// -Z
|
||||
North,
|
||||
/// +Z
|
||||
South,
|
||||
/// -X
|
||||
West,
|
||||
/// +X
|
||||
East,
|
||||
}
|
12
crates/valence_core/src/game_mode.rs
Normal file
12
crates/valence_core/src/game_mode.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use bevy_ecs::prelude::*;
|
||||
|
||||
use crate::packet::{Decode, Encode};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Encode, Decode, Component)]
|
||||
pub enum GameMode {
|
||||
#[default]
|
||||
Survival,
|
||||
Creative,
|
||||
Adventure,
|
||||
Spectator,
|
||||
}
|
8
crates/valence_core/src/hand.rs
Normal file
8
crates/valence_core/src/hand.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use crate::packet::{Decode, Encode};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug, Encode, Decode)]
|
||||
pub enum Hand {
|
||||
#[default]
|
||||
Main,
|
||||
Off,
|
||||
}
|
|
@ -11,7 +11,12 @@ use serde::de::Error as _;
|
|||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{nbt, Decode, Encode};
|
||||
use crate::packet::{Decode, Encode};
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod __private {
|
||||
pub use valence_core_macros::parse_ident_str;
|
||||
}
|
||||
|
||||
/// A wrapper around a string type `S` which guarantees the wrapped string is a
|
||||
/// valid resource identifier.
|
||||
|
@ -29,6 +34,28 @@ pub struct Ident<S> {
|
|||
string: S,
|
||||
}
|
||||
|
||||
/// Creates a new [`Ident`] at compile time from a string literal. A compile
|
||||
/// error is raised if the string is not a valid resource identifier.
|
||||
///
|
||||
/// The type of the expression returned by this macro is `Ident<&'static str>`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use valence_core::{ident, ident::Ident};
|
||||
/// let my_ident: Ident<&'static str> = ident!("apple");
|
||||
///
|
||||
/// println!("{my_ident}");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! ident {
|
||||
($string:literal) => {
|
||||
$crate::ident::Ident::<&'static str>::new_unchecked(
|
||||
$crate::ident::__private::parse_ident_str!($string),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// The error type created when an [`Ident`] cannot be parsed from a
|
||||
/// string. Contains the string that failed to parse.
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Error)]
|
||||
|
@ -307,9 +334,9 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<S> From<Ident<S>> for nbt::Value
|
||||
impl<S> From<Ident<S>> for valence_nbt::Value
|
||||
where
|
||||
S: Into<nbt::Value>,
|
||||
S: Into<valence_nbt::Value>,
|
||||
{
|
||||
fn from(value: Ident<S>) -> Self {
|
||||
value.into_inner().into()
|
||||
|
@ -341,7 +368,6 @@ where
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ident;
|
||||
|
||||
#[test]
|
||||
fn check_namespace_and_path() {
|
|
@ -3,9 +3,8 @@ use std::io::Write;
|
|||
use anyhow::{ensure, Context};
|
||||
use valence_nbt::Compound;
|
||||
|
||||
use crate::block::BlockKind;
|
||||
use crate::var_int::VarInt;
|
||||
use crate::{Decode, Encode, Result};
|
||||
use crate::packet::var_int::VarInt;
|
||||
use crate::packet::{Decode, Encode};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/item.rs"));
|
||||
|
||||
|
@ -16,15 +15,15 @@ pub struct ItemStack {
|
|||
pub nbt: Option<Compound>,
|
||||
}
|
||||
|
||||
pub const STACK_MIN: u8 = 1;
|
||||
pub const STACK_MAX: u8 = 127;
|
||||
|
||||
impl ItemStack {
|
||||
pub const STACK_MIN: u8 = 1;
|
||||
pub const STACK_MAX: u8 = 127;
|
||||
|
||||
#[must_use]
|
||||
pub fn new(item: ItemKind, count: u8, nbt: Option<Compound>) -> Self {
|
||||
Self {
|
||||
item,
|
||||
count: count.clamp(STACK_MIN, STACK_MAX),
|
||||
count: count.clamp(Self::STACK_MIN, Self::STACK_MAX),
|
||||
nbt,
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +54,7 @@ impl ItemStack {
|
|||
/// Sets the number of items in this stack. Values are clamped to 1-127,
|
||||
/// which are the positive values accepted by clients.
|
||||
pub fn set_count(&mut self, count: u8) {
|
||||
self.count = count.clamp(STACK_MIN, STACK_MAX);
|
||||
self.count = count.clamp(Self::STACK_MIN, Self::STACK_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,13 +65,13 @@ impl Default for ItemStack {
|
|||
}
|
||||
|
||||
impl Encode for Option<ItemStack> {
|
||||
fn encode(&self, w: impl Write) -> Result<()> {
|
||||
fn encode(&self, w: impl Write) -> anyhow::Result<()> {
|
||||
self.as_ref().encode(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encode for Option<&'a ItemStack> {
|
||||
fn encode(&self, mut w: impl Write) -> Result<()> {
|
||||
fn encode(&self, mut w: impl Write) -> anyhow::Result<()> {
|
||||
match *self {
|
||||
None => false.encode(w),
|
||||
Some(s) => {
|
||||
|
@ -89,7 +88,7 @@ impl<'a> Encode for Option<&'a ItemStack> {
|
|||
}
|
||||
|
||||
impl Decode<'_> for Option<ItemStack> {
|
||||
fn decode(r: &mut &[u8]) -> Result<Self> {
|
||||
fn decode(r: &mut &[u8]) -> anyhow::Result<Self> {
|
||||
let present = bool::decode(r)?;
|
||||
if !present {
|
||||
return Ok(None);
|
||||
|
@ -99,8 +98,10 @@ impl Decode<'_> for Option<ItemStack> {
|
|||
let count = u8::decode(r)?;
|
||||
|
||||
ensure!(
|
||||
(STACK_MIN..=STACK_MAX).contains(&count),
|
||||
"invalid item stack count (got {count}, expected {STACK_MIN}..={STACK_MAX})"
|
||||
(ItemStack::STACK_MIN..=ItemStack::STACK_MAX).contains(&count),
|
||||
"invalid item stack count (got {count}, expected {}..={})",
|
||||
ItemStack::STACK_MIN,
|
||||
ItemStack::STACK_MAX,
|
||||
);
|
||||
|
||||
let nbt = if let [0, rest @ ..] = *r {
|
||||
|
@ -115,13 +116,13 @@ impl Decode<'_> for Option<ItemStack> {
|
|||
}
|
||||
|
||||
impl Encode for ItemKind {
|
||||
fn encode(&self, w: impl Write) -> Result<()> {
|
||||
fn encode(&self, w: impl Write) -> anyhow::Result<()> {
|
||||
VarInt(self.to_raw() as i32).encode(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_> for ItemKind {
|
||||
fn decode(r: &mut &[u8]) -> Result<Self> {
|
||||
fn decode(r: &mut &[u8]) -> anyhow::Result<Self> {
|
||||
let id = VarInt::decode(r)?.0;
|
||||
let errmsg = "invalid item ID";
|
||||
|
||||
|
@ -129,9 +130,11 @@ impl Decode<'_> for ItemKind {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::block::BlockKind;
|
||||
|
||||
#[test]
|
||||
fn item_kind_to_block_kind() {
|
||||
|
@ -160,3 +163,4 @@ mod tests {
|
|||
assert_eq!(stack.count, STACK_MAX);
|
||||
}
|
||||
}
|
||||
*/
|
178
crates/valence_core/src/lib.rs
Normal file
178
crates/valence_core/src/lib.rs
Normal file
|
@ -0,0 +1,178 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![deny(
|
||||
rustdoc::broken_intra_doc_links,
|
||||
rustdoc::private_intra_doc_links,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
rustdoc::invalid_codeblock_attributes,
|
||||
rustdoc::invalid_rust_codeblocks,
|
||||
rustdoc::bare_urls,
|
||||
rustdoc::invalid_html_tags
|
||||
)]
|
||||
#![warn(
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unused_lifetimes,
|
||||
unused_import_braces,
|
||||
unreachable_pub,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
#![allow(clippy::unusual_byte_groupings)]
|
||||
|
||||
pub mod aabb;
|
||||
pub mod block_pos;
|
||||
pub mod chunk_pos;
|
||||
pub mod despawn;
|
||||
pub mod difficulty;
|
||||
pub mod direction;
|
||||
pub mod game_mode;
|
||||
pub mod hand;
|
||||
pub mod ident;
|
||||
pub mod item;
|
||||
pub mod packet;
|
||||
pub mod player_textures;
|
||||
pub mod property;
|
||||
pub mod scratch;
|
||||
pub mod sound;
|
||||
pub mod text;
|
||||
pub mod translation_key;
|
||||
pub mod uuid;
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
use std::time::Duration;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_app::{ScheduleRunnerPlugin, ScheduleRunnerSettings};
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
use crate::despawn::despawn_marked_entities;
|
||||
|
||||
/// Used only by macros. Not public API.
|
||||
#[doc(hidden)]
|
||||
pub mod __private {
|
||||
pub use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
|
||||
pub use crate::packet::var_int::VarInt;
|
||||
pub use crate::packet::{Decode, Encode, Packet};
|
||||
}
|
||||
|
||||
// Needed to make proc macros work.
|
||||
extern crate self as valence_core;
|
||||
|
||||
/// The Minecraft protocol version this library currently targets.
|
||||
pub const PROTOCOL_VERSION: i32 = 762;
|
||||
|
||||
/// The stringified name of the Minecraft version this library currently
|
||||
/// targets.
|
||||
pub const MINECRAFT_VERSION: &str = "1.19.4";
|
||||
|
||||
/// Minecraft's standard ticks per second (TPS).
|
||||
pub const DEFAULT_TPS: NonZeroU32 = match NonZeroU32::new(20) {
|
||||
Some(n) => n,
|
||||
None => unreachable!(),
|
||||
};
|
||||
|
||||
pub struct CorePlugin;
|
||||
|
||||
impl Plugin for CorePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
let settings = app.world.get_resource_or_insert_with(CoreSettings::default);
|
||||
|
||||
let compression_threshold = settings.compression_threshold;
|
||||
let tick_rate = settings.tick_rate;
|
||||
|
||||
app.insert_resource(Server {
|
||||
current_tick: 0,
|
||||
compression_threshold,
|
||||
});
|
||||
|
||||
let tick_period = Duration::from_secs_f64((tick_rate.get() as f64).recip());
|
||||
|
||||
// Make the app loop forever at the configured TPS.
|
||||
app.insert_resource(ScheduleRunnerSettings::run_loop(tick_period))
|
||||
.add_plugin(ScheduleRunnerPlugin);
|
||||
|
||||
fn increment_tick_counter(mut server: ResMut<Server>) {
|
||||
server.current_tick += 1;
|
||||
}
|
||||
|
||||
app.add_systems(
|
||||
(increment_tick_counter, despawn_marked_entities).in_base_set(CoreSet::Last),
|
||||
);
|
||||
|
||||
// TODO: do this in `DefaultPlugins`.
|
||||
/*
|
||||
// Add internal plugins.
|
||||
app.add_plugin(EventLoopPlugin)
|
||||
.add_plugin(RegistryCodecPlugin)
|
||||
.add_plugin(BiomePlugin)
|
||||
.add_plugin(DimensionPlugin)
|
||||
.add_plugin(ComponentPlugin)
|
||||
.add_plugin(ClientPlugin)
|
||||
.add_plugin(EntityPlugin)
|
||||
.add_plugin(InstancePlugin)
|
||||
.add_plugin(InventoryPlugin)
|
||||
.add_plugin(PlayerListPlugin)
|
||||
.add_plugin(WeatherPlugin);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Debug)]
|
||||
pub struct CoreSettings {
|
||||
/// The target ticks per second (TPS) of the server. This is the number of
|
||||
/// game updates that should occur in one second.
|
||||
///
|
||||
/// On each game update (tick), the server is expected to update game logic
|
||||
/// and respond to packets from clients. Once this is complete, the server
|
||||
/// will sleep for any remaining time until a full tick duration has passed.
|
||||
///
|
||||
/// Note that the official Minecraft client only processes packets at 20hz,
|
||||
/// so there is little benefit to a tick rate higher than the default 20.
|
||||
///
|
||||
/// # Default Value
|
||||
///
|
||||
/// [`DEFAULT_TPS`]
|
||||
pub tick_rate: NonZeroU32,
|
||||
/// The compression threshold to use for compressing packets. For a
|
||||
/// compression threshold of `Some(N)`, packets with encoded lengths >= `N`
|
||||
/// are compressed while all others are not. `None` disables compression
|
||||
/// 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 value. This value may
|
||||
/// change in future versions.
|
||||
pub compression_threshold: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for CoreSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tick_rate: DEFAULT_TPS,
|
||||
compression_threshold: Some(256),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains global server state accessible as a [`Resource`].
|
||||
#[derive(Resource)]
|
||||
pub struct Server {
|
||||
/// Incremented on every tick.
|
||||
current_tick: i64,
|
||||
compression_threshold: Option<u32>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Returns the number of ticks that have elapsed since the server began.
|
||||
pub fn current_tick(&self) -> i64 {
|
||||
self.current_tick
|
||||
}
|
||||
|
||||
/// Returns the server's compression threshold.
|
||||
pub fn compression_threshold(&self) -> Option<u32> {
|
||||
self.compression_threshold
|
||||
}
|
||||
}
|
|
@ -1,117 +1,23 @@
|
|||
//! A library for interacting with the Minecraft (Java Edition) network
|
||||
//! protocol.
|
||||
//! Types and functions used in Minecraft's packets. Structs for each packet are
|
||||
//! defined here too.
|
||||
//!
|
||||
//! The API is centered around the [`Encode`] and [`Decode`] traits. Clientbound
|
||||
//! and serverbound packets are defined in the [`packet`] module. Packets are
|
||||
//! encoded and decoded using the [`PacketEncoder`] and [`PacketDecoder`] types.
|
||||
//!
|
||||
//! [`PacketEncoder`]: encoder::PacketEncoder
|
||||
//! [`PacketDecoder`]: decoder::PacketDecoder
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```
|
||||
//! use valence_protocol::decoder::PacketDecoder;
|
||||
//! use valence_protocol::encoder::PacketEncoder;
|
||||
//! use valence_protocol::packet::c2s::play::RenameItemC2s;
|
||||
//! use valence_protocol::Packet;
|
||||
//!
|
||||
//! let mut enc = PacketEncoder::new();
|
||||
//!
|
||||
//! let outgoing = RenameItemC2s {
|
||||
//! item_name: "Hello!",
|
||||
//! };
|
||||
//!
|
||||
//! enc.append_packet(&outgoing).unwrap();
|
||||
//!
|
||||
//! let mut dec = PacketDecoder::new();
|
||||
//!
|
||||
//! dec.queue_bytes(enc.take());
|
||||
//!
|
||||
//! let frame = dec.try_next_packet().unwrap().unwrap();
|
||||
//!
|
||||
//! let incoming = RenameItemC2s::decode_packet(&mut &frame[..]).unwrap();
|
||||
//!
|
||||
//! assert_eq!(outgoing.item_name, incoming.item_name);
|
||||
//! ```
|
||||
//!
|
||||
//! # Stability
|
||||
//!
|
||||
//! The Minecraft protocol is not stable. Updates to Minecraft may change the
|
||||
//! protocol in subtle or drastic ways. In response to this, `valence_protocol`
|
||||
//! aims to support only the most recent version of the game (excluding
|
||||
//! snapshots, pre-releases, etc). An update to Minecraft often requires a
|
||||
//! breaking change to the library.
|
||||
//!
|
||||
//! `valence_protocol` is versioned in lockstep with `valence`. The currently
|
||||
//! supported Minecraft version can be checked with the [`PROTOCOL_VERSION`] or
|
||||
//! [`MINECRAFT_VERSION`] constants.
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//!
|
||||
//! TODO
|
||||
|
||||
#![deny(
|
||||
rustdoc::broken_intra_doc_links,
|
||||
rustdoc::private_intra_doc_links,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
rustdoc::invalid_codeblock_attributes,
|
||||
rustdoc::invalid_rust_codeblocks,
|
||||
rustdoc::bare_urls
|
||||
)]
|
||||
#![warn(
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unused_lifetimes,
|
||||
unused_import_braces,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
#![allow(clippy::unusual_byte_groupings)]
|
||||
|
||||
// Allows us to use our own proc macros internally.
|
||||
extern crate self as valence_protocol;
|
||||
|
||||
use std::fmt;
|
||||
use std::io::Write;
|
||||
|
||||
pub use anyhow::{Error, Result};
|
||||
pub use valence_protocol_macros::{ident, Decode, Encode, Packet};
|
||||
pub use {bytes, uuid, valence_nbt as nbt};
|
||||
|
||||
/// The Minecraft protocol version this library currently targets.
|
||||
pub const PROTOCOL_VERSION: i32 = 762;
|
||||
|
||||
/// The stringified name of the Minecraft version this library currently
|
||||
/// targets.
|
||||
pub const MINECRAFT_VERSION: &str = "1.19.4";
|
||||
//! Client-to-server packets live in [`c2s`] while server-to-client packets are
|
||||
//! in [`s2c`].
|
||||
|
||||
pub mod array;
|
||||
pub mod block;
|
||||
pub mod block_pos;
|
||||
pub mod byte_angle;
|
||||
pub mod decoder;
|
||||
pub mod enchant;
|
||||
pub mod encoder;
|
||||
pub mod ident;
|
||||
mod impls;
|
||||
pub mod item;
|
||||
pub mod packet;
|
||||
pub mod decode;
|
||||
pub mod encode;
|
||||
pub mod global_pos;
|
||||
pub mod impls;
|
||||
pub mod message_signature;
|
||||
pub mod raw;
|
||||
pub mod sound;
|
||||
pub mod text;
|
||||
pub mod translation_key;
|
||||
pub mod types;
|
||||
pub mod var_int;
|
||||
pub mod var_long;
|
||||
|
||||
/// Used only by proc macros. Not public API.
|
||||
#[doc(hidden)]
|
||||
pub mod __private {
|
||||
pub use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use std::io::Write;
|
||||
|
||||
pub use crate::var_int::VarInt;
|
||||
pub use crate::{Decode, Encode, Packet};
|
||||
}
|
||||
pub use valence_core_macros::{Decode, Encode, Packet};
|
||||
|
||||
/// The maximum number of bytes in a single Minecraft packet.
|
||||
pub const MAX_PACKET_SIZE: i32 = 2097152;
|
||||
|
@ -132,7 +38,7 @@ pub const MAX_PACKET_SIZE: i32 = 2097152;
|
|||
/// to variants using rules similar to regular enum discriminants.
|
||||
///
|
||||
/// ```
|
||||
/// use valence_protocol::Encode;
|
||||
/// use valence_core::packet::Encode;
|
||||
///
|
||||
/// #[derive(Encode)]
|
||||
/// struct MyStruct<'a> {
|
||||
|
@ -162,7 +68,7 @@ pub const MAX_PACKET_SIZE: i32 = 2097152;
|
|||
/// println!("{buf:?}");
|
||||
/// ```
|
||||
///
|
||||
/// [macro]: valence_protocol_macros::Encode
|
||||
/// [macro]: valence_core_macros::Encode
|
||||
/// [`VarInt`]: var_int::VarInt
|
||||
pub trait Encode {
|
||||
/// Writes this object to the provided writer.
|
||||
|
@ -173,21 +79,30 @@ pub trait Encode {
|
|||
/// that were originally written must be consumed during the decoding.
|
||||
///
|
||||
/// [`decode`]: Decode::decode
|
||||
fn encode(&self, w: impl Write) -> Result<()>;
|
||||
fn encode(&self, w: impl Write) -> anyhow::Result<()>;
|
||||
|
||||
/// Hack to get around the lack of specialization. Not public API.
|
||||
#[doc(hidden)]
|
||||
fn encode_slice(slice: &[Self], w: impl Write) -> Result<()>
|
||||
/// Like [`Encode::encode`], except that a whole slice of values is encoded.
|
||||
///
|
||||
/// This method must be semantically equivalent to encoding every element of
|
||||
/// the slice in sequence with no leading length prefix (which is exactly
|
||||
/// what the default implementation does), but a more efficient
|
||||
/// implementation may be used.
|
||||
///
|
||||
/// This optimization is very important for some types like `u8` where
|
||||
/// [`write_all`] is used. Because impl specialization is unavailable in
|
||||
/// stable Rust, we must make the slice specialization part of this trait.
|
||||
///
|
||||
/// [`write_all`]: Write::write_all
|
||||
fn encode_slice(slice: &[Self], mut w: impl Write) -> anyhow::Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let _ = (slice, w);
|
||||
unimplemented!("no implementation of `encode_slice`")
|
||||
for value in slice {
|
||||
value.encode(&mut w)?;
|
||||
}
|
||||
|
||||
/// Hack to get around the lack of specialization. Not public API.
|
||||
#[doc(hidden)]
|
||||
const HAS_ENCODE_SLICE: bool = false;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Decode` trait allows objects to be read from the Minecraft protocol. It
|
||||
|
@ -209,7 +124,7 @@ pub trait Encode {
|
|||
/// to variants using rules similar to regular enum discriminants.
|
||||
///
|
||||
/// ```
|
||||
/// use valence_protocol::Decode;
|
||||
/// use valence_core::packet::Decode;
|
||||
///
|
||||
/// #[derive(PartialEq, Debug, Decode)]
|
||||
/// struct MyStruct {
|
||||
|
@ -238,14 +153,14 @@ pub trait Encode {
|
|||
/// assert!(r.is_empty());
|
||||
/// ```
|
||||
///
|
||||
/// [macro]: valence_protocol_macros::Decode
|
||||
/// [macro]: valence_core_macros::Decode
|
||||
/// [`VarInt`]: var_int::VarInt
|
||||
pub trait Decode<'a>: Sized {
|
||||
/// Reads this object from the provided byte slice.
|
||||
///
|
||||
/// Implementations of `Decode` are expected to shrink the slice from the
|
||||
/// front as bytes are read.
|
||||
fn decode(r: &mut &'a [u8]) -> Result<Self>;
|
||||
fn decode(r: &mut &'a [u8]) -> anyhow::Result<Self>;
|
||||
}
|
||||
|
||||
/// Like [`Encode`] + [`Decode`], but implementations must read and write a
|
||||
|
@ -257,10 +172,10 @@ pub trait Decode<'a>: Sized {
|
|||
/// [`Packet`][macro] derive macro. The trait is implemented by reading or
|
||||
/// writing the packet ID provided in the `#[packet_id = ...]` helper attribute
|
||||
/// followed by a call to [`Encode::encode`] or [`Decode::decode`]. The target
|
||||
/// type must implement [`Encode`], [`Decode`], and [`fmt::Debug`].
|
||||
/// type must implement [`Encode`], [`Decode`], and [`std::fmt::Debug`].
|
||||
///
|
||||
/// ```
|
||||
/// use valence_protocol::{Decode, Encode, Packet};
|
||||
/// use valence_core::packet::{Decode, Encode, Packet};
|
||||
///
|
||||
/// #[derive(Encode, Decode, Packet, Debug)]
|
||||
/// #[packet_id = 42]
|
||||
|
@ -275,9 +190,9 @@ pub trait Decode<'a>: Sized {
|
|||
/// println!("{buf:?}");
|
||||
/// ```
|
||||
///
|
||||
/// [macro]: valence_protocol_macros::Packet
|
||||
/// [macro]: valence_core_macros::Packet
|
||||
/// [`VarInt`]: var_int::VarInt
|
||||
pub trait Packet<'a>: Sized + fmt::Debug {
|
||||
pub trait Packet<'a>: Sized + std::fmt::Debug {
|
||||
/// The packet returned by [`Self::packet_id`]. If the packet ID is not
|
||||
/// statically known, then a negative value is used instead.
|
||||
const PACKET_ID: i32 = -1;
|
||||
|
@ -287,15 +202,251 @@ pub trait Packet<'a>: Sized + fmt::Debug {
|
|||
/// additional formatting.
|
||||
fn packet_name(&self) -> &str;
|
||||
/// Like [`Encode::encode`], but a leading [`VarInt`] packet ID must be
|
||||
/// written first.
|
||||
/// written by this method first.
|
||||
///
|
||||
/// [`VarInt`]: var_int::VarInt
|
||||
fn encode_packet(&self, w: impl Write) -> Result<()>;
|
||||
fn encode_packet(&self, w: impl Write) -> anyhow::Result<()>;
|
||||
/// Like [`Decode::decode`], but a leading [`VarInt`] packet ID must be read
|
||||
/// first.
|
||||
/// by this method first.
|
||||
///
|
||||
/// [`VarInt`]: var_int::VarInt
|
||||
fn decode_packet(r: &mut &'a [u8]) -> Result<Self>;
|
||||
fn decode_packet(r: &mut &'a [u8]) -> anyhow::Result<Self>;
|
||||
}
|
||||
|
||||
/// Defines an enum of packets and implements [`Packet`] for each.
|
||||
macro_rules! packet_group {
|
||||
(
|
||||
$(#[$attrs:meta])*
|
||||
$enum_name:ident<$enum_life:lifetime> {
|
||||
$($packet:ident $(<$life:lifetime>)?),* $(,)?
|
||||
}
|
||||
) => {
|
||||
$(#[$attrs])*
|
||||
pub enum $enum_name<$enum_life> {
|
||||
$(
|
||||
$packet($packet $(<$life>)?),
|
||||
)*
|
||||
}
|
||||
|
||||
$(
|
||||
impl<$enum_life> From<$packet $(<$life>)?> for $enum_name<$enum_life> {
|
||||
fn from(p: $packet $(<$life>)?) -> Self {
|
||||
Self::$packet(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl<$enum_life> $crate::packet::Packet<$enum_life> for $packet$(<$life>)? {
|
||||
const PACKET_ID: i32 = $crate::packet::id::$packet;
|
||||
|
||||
fn packet_id(&self) -> i32 {
|
||||
Self::PACKET_ID
|
||||
}
|
||||
|
||||
fn packet_name(&self) -> &str {
|
||||
stringify!($packet)
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
fn encode_packet(&self, mut w: impl std::io::Write) -> $crate::__private::Result<()> {
|
||||
use $crate::__private::*;
|
||||
|
||||
VarInt(Self::PACKET_ID)
|
||||
.encode(&mut w)
|
||||
.context("failed to encode packet ID")?;
|
||||
|
||||
self.encode(w)
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
fn decode_packet(r: &mut &$enum_life [u8]) -> $crate::__private::Result<Self> {
|
||||
use $crate::__private::*;
|
||||
|
||||
let id = VarInt::decode(r).context("failed to decode packet ID")?.0;
|
||||
ensure!(id == Self::PACKET_ID, "unexpected packet ID {} (expected {})", id, Self::PACKET_ID);
|
||||
|
||||
Self::decode(r)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
impl<$enum_life> $crate::packet::Packet<$enum_life> for $enum_name<$enum_life> {
|
||||
fn packet_id(&self) -> i32 {
|
||||
match self {
|
||||
$(
|
||||
Self::$packet(_) => <$packet as $crate::packet::Packet>::PACKET_ID,
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn packet_name(&self) -> &str {
|
||||
match self {
|
||||
$(
|
||||
Self::$packet(pkt) => pkt.packet_name(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_packet(&self, mut w: impl std::io::Write) -> $crate::__private::Result<()> {
|
||||
use $crate::__private::*;
|
||||
|
||||
match self {
|
||||
$(
|
||||
Self::$packet(pkt) => {
|
||||
VarInt(<$packet as Packet>::PACKET_ID).encode(&mut w)?;
|
||||
pkt.encode(w)?;
|
||||
}
|
||||
)*
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_packet(r: &mut &$enum_life [u8]) -> $crate::__private::Result<Self> {
|
||||
use $crate::__private::*;
|
||||
|
||||
let id = VarInt::decode(r)?.0;
|
||||
Ok(match id {
|
||||
$(
|
||||
<$packet as Packet>::PACKET_ID =>
|
||||
Self::$packet($packet::decode(r)?),
|
||||
)*
|
||||
id => bail!("unknown packet ID {} while decoding {}", id, stringify!($enum_name)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<$enum_life> std::fmt::Debug for $enum_name<$enum_life> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
$(
|
||||
Self::$packet(pkt) => pkt.fmt(f),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// No lifetime on the enum in this case.
|
||||
(
|
||||
$(#[$attrs:meta])*
|
||||
$enum_name:ident {
|
||||
$($packet:ident),* $(,)?
|
||||
}
|
||||
) => {
|
||||
$(#[$attrs])*
|
||||
pub enum $enum_name {
|
||||
$(
|
||||
$packet($packet),
|
||||
)*
|
||||
}
|
||||
|
||||
$(
|
||||
impl From<$packet> for $enum_name {
|
||||
fn from(p: $packet) -> Self {
|
||||
Self::$packet(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::__private::Packet<'_> for $packet {
|
||||
const PACKET_ID: i32 = $crate::packet::id::$packet;
|
||||
|
||||
fn packet_id(&self) -> i32 {
|
||||
Self::PACKET_ID
|
||||
}
|
||||
|
||||
fn packet_name(&self) -> &str {
|
||||
stringify!($packet)
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
fn encode_packet(&self, mut w: impl std::io::Write) -> $crate::__private::Result<()> {
|
||||
use $crate::__private::*;
|
||||
|
||||
VarInt(Self::PACKET_ID)
|
||||
.encode(&mut w)
|
||||
.context("failed to encode packet ID")?;
|
||||
|
||||
self.encode(w)
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
fn decode_packet(r: &mut &[u8]) -> $crate::__private::Result<Self> {
|
||||
use $crate::__private::*;
|
||||
|
||||
let id = VarInt::decode(r).context("failed to decode packet ID")?.0;
|
||||
ensure!(id == Self::PACKET_ID, "unexpected packet ID {} (expected {})", id, Self::PACKET_ID);
|
||||
|
||||
Self::decode(r)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
impl $crate::__private::Packet<'_> for $enum_name {
|
||||
fn packet_id(&self) -> i32 {
|
||||
use $crate::__private::*;
|
||||
|
||||
match self {
|
||||
$(
|
||||
Self::$packet(_) => <$packet as Packet>::PACKET_ID,
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn packet_name(&self) -> &str {
|
||||
match self {
|
||||
$(
|
||||
Self::$packet(pkt) => pkt.packet_name(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_packet(&self, mut w: impl std::io::Write) -> $crate::__private::Result<()> {
|
||||
use $crate::__private::*;
|
||||
|
||||
match self {
|
||||
$(
|
||||
Self::$packet(pkt) => {
|
||||
VarInt(<$packet as Packet>::PACKET_ID).encode(&mut w)?;
|
||||
pkt.encode(w)?;
|
||||
}
|
||||
)*
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_packet(r: &mut &[u8]) -> $crate::__private::Result<Self> {
|
||||
use $crate::__private::*;
|
||||
|
||||
let id = VarInt::decode(r)?.0;
|
||||
Ok(match id {
|
||||
$(
|
||||
<$packet as Packet>::PACKET_ID =>
|
||||
Self::$packet($packet::decode(r)?),
|
||||
)*
|
||||
id => anyhow::bail!("unknown packet ID {} while decoding {}", id, stringify!($enum_name)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for $enum_name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
$(
|
||||
Self::$packet(pkt) => pkt.fmt(f),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod c2s;
|
||||
pub mod s2c;
|
||||
|
||||
/// Contains the packet ID for every packet. Because the constants are private
|
||||
/// to the crate, the compiler will yell at us when we forget to use one.
|
||||
mod id {
|
||||
include!(concat!(env!("OUT_DIR"), "/packet_id.rs"));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
@ -306,10 +457,9 @@ mod tests {
|
|||
use bytes::BytesMut;
|
||||
|
||||
use super::*;
|
||||
use crate::decoder::{decode_packet, PacketDecoder};
|
||||
use crate::encoder::PacketEncoder;
|
||||
use crate::packet::c2s::play::HandSwingC2s;
|
||||
use crate::packet::C2sPlayPacket;
|
||||
use crate::packet::c2s::play::{C2sPlayPacket, HandSwingC2s};
|
||||
use crate::packet::decode::{decode_packet, PacketDecoder};
|
||||
use crate::packet::encode::PacketEncoder;
|
||||
|
||||
#[derive(Encode, Decode, Packet, Debug)]
|
||||
#[packet_id = 1]
|
||||
|
@ -368,7 +518,7 @@ mod tests {
|
|||
Fourth(T),
|
||||
}
|
||||
|
||||
#[allow(unconditional_recursion)]
|
||||
#[allow(unconditional_recursion, clippy::extra_unused_type_parameters)]
|
||||
fn assert_has_impls<'a, T>()
|
||||
where
|
||||
T: Encode + Decode<'a> + Packet<'a>,
|
||||
|
@ -406,12 +556,12 @@ mod tests {
|
|||
}
|
||||
|
||||
use crate::block_pos::BlockPos;
|
||||
use crate::hand::Hand;
|
||||
use crate::ident::Ident;
|
||||
use crate::item::{ItemKind, ItemStack};
|
||||
use crate::packet::var_int::VarInt;
|
||||
use crate::packet::var_long::VarLong;
|
||||
use crate::text::{Text, TextFormat};
|
||||
use crate::types::Hand;
|
||||
use crate::var_int::VarInt;
|
||||
use crate::var_long::VarLong;
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
const CRYPT_KEY: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
|
|
@ -2,8 +2,8 @@ use std::io::Write;
|
|||
|
||||
use anyhow::ensure;
|
||||
|
||||
use crate::var_int::VarInt;
|
||||
use crate::{Decode, Encode};
|
||||
use crate::packet::var_int::VarInt;
|
||||
use crate::packet::{Decode, Encode};
|
||||
|
||||
/// A fixed-size array encoded and decoded with a [`VarInt`] length prefix.
|
||||
///
|
|
@ -1,7 +1,7 @@
|
|||
use std::f32::consts::TAU;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::{Decode, Encode, Result};
|
||||
use crate::packet::{Decode, Encode};
|
||||
|
||||
/// Represents an angle in steps of 1/256 of a full turn.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
|
@ -26,13 +26,13 @@ impl ByteAngle {
|
|||
}
|
||||
|
||||
impl Encode for ByteAngle {
|
||||
fn encode(&self, w: impl Write) -> Result<()> {
|
||||
fn encode(&self, w: impl Write) -> anyhow::Result<()> {
|
||||
self.0.encode(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_> for ByteAngle {
|
||||
fn decode(r: &mut &[u8]) -> Result<Self> {
|
||||
fn decode(r: &mut &[u8]) -> anyhow::Result<Self> {
|
||||
u8::decode(r).map(ByteAngle)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use crate::var_int::VarInt;
|
||||
use crate::{Decode, Encode};
|
||||
use crate::packet::var_int::VarInt;
|
||||
use crate::packet::{Decode, Encode};
|
||||
|
||||
#[derive(Clone, Debug, Encode, Decode)]
|
||||
pub struct HandshakeC2s<'a> {
|
|
@ -1,6 +1,6 @@
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::{Decode, Encode};
|
||||
use crate::packet::{Decode, Encode};
|
||||
|
||||
#[derive(Clone, Debug, Encode, Decode)]
|
||||
pub struct LoginHelloC2s<'a> {
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue