mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 22:41:30 +11:00
move from private repo
This commit is contained in:
parent
db0eae3a6c
commit
5bbbb258d1
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
|
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -1,7 +0,0 @@
|
||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "valence"
|
|
||||||
version = "0.0.1"
|
|
45
Cargo.toml
45
Cargo.toml
|
@ -1,9 +1,50 @@
|
||||||
[package]
|
[package]
|
||||||
name = "valence"
|
name = "valence"
|
||||||
version = "0.0.1"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Coming soon to a package manager near you!"
|
description = "A framework for building Minecraft servers in Rust."
|
||||||
repository = "https://github.com/rj00a/valence"
|
repository = "https://github.com/rj00a/valence"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["valence-data"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
aes = "0.7"
|
||||||
|
anyhow = "1"
|
||||||
|
ascii = "1"
|
||||||
|
async-trait = "0.1"
|
||||||
|
base64 = "0.13"
|
||||||
|
bitvec = "1"
|
||||||
|
bytes = "1"
|
||||||
|
byteorder = "1"
|
||||||
|
cfb8 = "0.7"
|
||||||
|
flate2 = "1"
|
||||||
|
flume = "0.10"
|
||||||
|
futures = "0.3"
|
||||||
|
hematite-nbt = "0.5"
|
||||||
|
log = "0.4"
|
||||||
|
nalgebra-glm = "0.16"
|
||||||
|
num = "0.4"
|
||||||
|
parking_lot = "0.12"
|
||||||
|
paste = "1"
|
||||||
|
rand = "0.8"
|
||||||
|
rayon = "1"
|
||||||
|
rsa = "0.5"
|
||||||
|
rsa-der = "0.3"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
sha1 = "0.10"
|
||||||
|
sha2 = "0.10"
|
||||||
|
thiserror = "1"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
uuid = "0.8"
|
||||||
|
|
||||||
|
[dependencies.reqwest]
|
||||||
|
version = "0.11"
|
||||||
|
default-features = false
|
||||||
|
# Avoid OpenSSL dependency on Linux.
|
||||||
|
features = ["rustls-tls", "json"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.9"
|
99
examples/basic.rs
Normal file
99
examples/basic.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use log::LevelFilter;
|
||||||
|
use valence::config::{Handler, ServerListPing};
|
||||||
|
use valence::text::Color;
|
||||||
|
use valence::{glm, DimensionId, Server, ServerConfig, SharedServer, ShutdownResult, TextFormat};
|
||||||
|
|
||||||
|
pub fn main() -> ShutdownResult {
|
||||||
|
env_logger::Builder::new()
|
||||||
|
.filter_level(LevelFilter::Trace)
|
||||||
|
.parse_default_env()
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let game = Game {
|
||||||
|
favicon: Arc::from(include_bytes!("favicon.png").as_slice()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cfg = ServerConfig::new();
|
||||||
|
|
||||||
|
cfg.handler(game);
|
||||||
|
cfg.online_mode(false);
|
||||||
|
cfg.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Game {
|
||||||
|
favicon: Arc<[u8]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handler for Game {
|
||||||
|
fn init(&self, server: &mut Server) {
|
||||||
|
let id = server.worlds.create(DimensionId::default());
|
||||||
|
let mut worlds = server.worlds.worlds_mut().unwrap();
|
||||||
|
let world = server.worlds.get(&mut worlds, id).unwrap();
|
||||||
|
|
||||||
|
let chunk_radius = 5;
|
||||||
|
|
||||||
|
for z in -chunk_radius..chunk_radius {
|
||||||
|
for x in -chunk_radius..chunk_radius {
|
||||||
|
let id = server.chunks.create(384);
|
||||||
|
let mut chunks = server.chunks.chunks_mut().unwrap();
|
||||||
|
|
||||||
|
let chunk = server.chunks.get(&mut chunks, id).unwrap();
|
||||||
|
|
||||||
|
for z in 0..16 {
|
||||||
|
for x in 0..16 {
|
||||||
|
for y in 0..50 {
|
||||||
|
chunk.set_block_state(x, y, z, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
world.chunks_mut().insert((x, z).into(), id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn server_list_ping(
|
||||||
|
&self,
|
||||||
|
server: &SharedServer,
|
||||||
|
_remote_addr: SocketAddr,
|
||||||
|
) -> ServerListPing {
|
||||||
|
ServerListPing::Respond {
|
||||||
|
online_players: server.client_count() as i32,
|
||||||
|
max_players: server.max_clients() as i32,
|
||||||
|
description: "Hello Valence!".color(Color::AQUA),
|
||||||
|
favicon_png: Some(self.favicon.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&self, server: &mut Server) {
|
||||||
|
let mut clients = server.entities.clients_mut().unwrap();
|
||||||
|
|
||||||
|
let world_id = server.worlds.ids().nth(0).unwrap();
|
||||||
|
|
||||||
|
let mut to_remove = Vec::new();
|
||||||
|
|
||||||
|
for (client_id, client) in server.entities.iter(&mut clients) {
|
||||||
|
if let Some(client) = client.get_mut() {
|
||||||
|
if client.created_tick() == server.current_tick() {
|
||||||
|
client.set_world(world_id);
|
||||||
|
client.teleport(glm::vec3(0.0, 200.0, 0.0), 0.0, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.is_disconnected() {
|
||||||
|
to_remove.push(client_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(clients);
|
||||||
|
|
||||||
|
for id in to_remove {
|
||||||
|
server.entities.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
examples/favicon.png
Normal file
BIN
examples/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
12
rustfmt.toml
Normal file
12
rustfmt.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
edition = "2021"
|
||||||
|
unstable_features = true
|
||||||
|
wrap_comments = true
|
||||||
|
imports_granularity = "Module"
|
||||||
|
reorder_impl_items = true
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
format_code_in_doc_comments = true
|
||||||
|
format_macro_matchers = true
|
||||||
|
hex_literal_case = "Lower"
|
||||||
|
format_strings = true
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
use_try_shorthand = true
|
64
src/aabb.rs
Normal file
64
src/aabb.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
use crate::glm::{self, Number, RealNumber, TVec};
|
||||||
|
|
||||||
|
/// An Axis-aligned bounding box in an arbitrary dimension.
|
||||||
|
#[derive(Clone, Copy, Debug)] // TODO: impl PartialEq, Eq, PartialOrd, Ord, Hash
|
||||||
|
pub struct Aabb<T, const D: usize> {
|
||||||
|
min: TVec<T, D>,
|
||||||
|
max: TVec<T, D>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Number, const D: usize> Aabb<T, D> {
|
||||||
|
pub fn new(p0: TVec<T, D>, p1: TVec<T, D>) -> Self {
|
||||||
|
Self {
|
||||||
|
min: glm::min2(&p0, &p1),
|
||||||
|
max: glm::max2(&p0, &p1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn point(pos: TVec<T, D>) -> Self {
|
||||||
|
Self { min: pos, max: pos }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min(&self) -> TVec<T, D> {
|
||||||
|
self.min
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max(&self) -> TVec<T, D> {
|
||||||
|
self.max
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dimensions(&self) -> TVec<T, D> {
|
||||||
|
self.max - self.min
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collides_with_aabb(&self, other: &Self) -> bool {
|
||||||
|
let l = glm::less_than_equal(&self.min, &other.max);
|
||||||
|
let r = glm::greater_than_equal(&self.max, &other.min);
|
||||||
|
glm::all(&l.zip_map(&r, |l, r| l && r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: RealNumber, const D: usize> Aabb<T, D> {
|
||||||
|
/// Construct an AABB from a center (centroid) and the dimensions of the box
|
||||||
|
/// along each axis.
|
||||||
|
pub fn from_center_and_dimensions(center: TVec<T, D>, dims: TVec<T, D>) -> Self {
|
||||||
|
let half = dims * T::from_subset(&0.5);
|
||||||
|
Self {
|
||||||
|
min: center - half,
|
||||||
|
max: center + half,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center(&self) -> TVec<T, D> {
|
||||||
|
// TODO: distribute multiplication to avoid intermediate overflow?
|
||||||
|
(self.min + self.max) * T::from_subset(&0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collides_with_sphere(&self, center: TVec<T, D>, radius: T) -> bool {
|
||||||
|
self.distance_to_point(center) <= radius
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn distance_to_point(&self, p: TVec<T, D>) -> T {
|
||||||
|
glm::distance(&p, &glm::clamp_vec(&p, &self.min, &self.max))
|
||||||
|
}
|
||||||
|
}
|
95
src/block_pos.rs
Normal file
95
src/block_pos.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
// TODO: rename to BlockPos and represent internally as [i32; 3].
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use glm::{IVec3, Scalar, TVec3};
|
||||||
|
use num::cast::AsPrimitive;
|
||||||
|
|
||||||
|
use crate::glm;
|
||||||
|
use crate::protocol::{Decode, Encode};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct BlockPos(pub IVec3);
|
||||||
|
|
||||||
|
impl BlockPos {
|
||||||
|
pub fn new(x: i32, y: i32, z: i32) -> Self {
|
||||||
|
Self(glm::vec3(x, y, z))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_vec3(vec: TVec3<impl Scalar + AsPrimitive<i32>>) -> Self {
|
||||||
|
Self(vec.map(|n| n.as_()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Scalar + Into<i32>> From<TVec3<T>> for BlockPos {
|
||||||
|
fn from(vec: TVec3<T>) -> Self {
|
||||||
|
Self(vec.map(|n| n.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for BlockPos {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
match (self.0.x, self.0.y, self.0.z) {
|
||||||
|
(-0x2000000..=0x1ffffff, -0x800..=0x7ff, -0x2000000..=0x1ffffff) => {
|
||||||
|
let (x, y, z) = (self.0.x as u64, self.0.y as u64, self.0.z as u64);
|
||||||
|
(x << 38 | z << 38 >> 26 | y & 0xfff).encode(w)
|
||||||
|
}
|
||||||
|
_ => bail!("block position {} is out of range", self.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for BlockPos {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
// Use arithmetic right shift to determine sign.
|
||||||
|
let val = i64::decode(r)?;
|
||||||
|
let x = val >> 38;
|
||||||
|
let z = val << 26 >> 38;
|
||||||
|
let y = val << 52 >> 52;
|
||||||
|
Ok(Self(glm::vec3(x as i32, y as i32, z as i32)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn position() {
|
||||||
|
let xzs = [
|
||||||
|
(-33554432, true),
|
||||||
|
(-33554433, false),
|
||||||
|
(33554431, true),
|
||||||
|
(33554432, false),
|
||||||
|
(0, true),
|
||||||
|
(1, true),
|
||||||
|
(-1, true),
|
||||||
|
];
|
||||||
|
let ys = [
|
||||||
|
(-2048, true),
|
||||||
|
(-2049, false),
|
||||||
|
(2047, true),
|
||||||
|
(2048, false),
|
||||||
|
(0, true),
|
||||||
|
(1, true),
|
||||||
|
(-1, true),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut buf = [0; 8];
|
||||||
|
|
||||||
|
for (x, x_valid) in xzs {
|
||||||
|
for (y, y_valid) in ys {
|
||||||
|
for (z, z_valid) in xzs {
|
||||||
|
let pos = BlockPos(glm::vec3(x, y, z));
|
||||||
|
if x_valid && y_valid && z_valid {
|
||||||
|
pos.encode(&mut &mut buf[..]).unwrap();
|
||||||
|
assert_eq!(BlockPos::decode(&mut &buf[..]).unwrap(), pos);
|
||||||
|
} else {
|
||||||
|
assert!(pos.encode(&mut &mut buf[..]).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
src/byte_angle.rs
Normal file
32
src/byte_angle.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// TODO: rename to ByteAngle?
|
||||||
|
|
||||||
|
use std::f64::consts::TAU;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use crate::protocol::{Decode, Encode};
|
||||||
|
|
||||||
|
/// Represents an angle in steps of 1/256 of a full turn.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
|
pub struct ByteAngle(pub u8);
|
||||||
|
|
||||||
|
impl ByteAngle {
|
||||||
|
pub fn from_radians_f64(f: f64) -> ByteAngle {
|
||||||
|
ByteAngle((f.rem_euclid(TAU) / TAU * 256.0).round() as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_radians_f64(self) -> f64 {
|
||||||
|
self.0 as f64 / 256.0 * TAU
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for ByteAngle {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
self.0.encode(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for ByteAngle {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(ByteAngle(u8::decode(r)?))
|
||||||
|
}
|
||||||
|
}
|
359
src/chunk.rs
Normal file
359
src/chunk.rs
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
// TODO: https://github.com/rust-lang/rust/issues/88581 for div_ceil
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use bitvec::bitvec;
|
||||||
|
use bitvec::vec::BitVec;
|
||||||
|
use num::Integer;
|
||||||
|
|
||||||
|
use crate::glm::DVec2;
|
||||||
|
use crate::packets::play::{
|
||||||
|
BlockChange, ChunkDataAndUpdateLight, ChunkDataHeightmaps, ClientPlayPacket, MultiBlockChange,
|
||||||
|
};
|
||||||
|
use crate::protocol::{Encode, Nbt};
|
||||||
|
use crate::var_int::VarInt;
|
||||||
|
use crate::BiomeId;
|
||||||
|
|
||||||
|
pub struct Chunk {
|
||||||
|
sections: Box<[ChunkSection]>,
|
||||||
|
// TODO block_entities: HashMap<u32, BlockEntity>,
|
||||||
|
heightmap: Vec<i64>,
|
||||||
|
modified: bool,
|
||||||
|
created_this_tick: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Chunk {
|
||||||
|
pub(crate) fn new(num_sections: usize) -> Self {
|
||||||
|
let sect = ChunkSection {
|
||||||
|
blocks: [0; 4096],
|
||||||
|
biomes: [0; 64],
|
||||||
|
compact_data: Vec::new(),
|
||||||
|
modified: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut chunk = Self {
|
||||||
|
sections: vec![sect; num_sections].into(),
|
||||||
|
heightmap: Vec::new(),
|
||||||
|
modified: true,
|
||||||
|
created_this_tick: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
chunk.apply_modifications();
|
||||||
|
chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn deallocate(&mut self) {
|
||||||
|
self.sections = Box::new([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn created_this_tick(&self) -> bool {
|
||||||
|
self.created_this_tick
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_created_this_tick(&mut self) {
|
||||||
|
self.created_this_tick = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(&self) -> usize {
|
||||||
|
self.sections.len() * 16
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_block_state(&self, x: usize, y: usize, z: usize) -> u16 {
|
||||||
|
if x < 16 && y < self.height() && z < 16 {
|
||||||
|
self.sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16]
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: u16) {
|
||||||
|
if x < 16 && y < self.height() && z < 16 {
|
||||||
|
let sec = &mut self.sections[y / 16];
|
||||||
|
let idx = x + z * 16 + y % 16 * 16 * 16;
|
||||||
|
if block != sec.blocks[idx] {
|
||||||
|
sec.blocks[idx] = block;
|
||||||
|
// TODO: set the modified bit.
|
||||||
|
sec.modified = true;
|
||||||
|
self.modified = true;
|
||||||
|
// TODO: update block entity if b could have block entity data.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_biome(&self, x: usize, y: usize, z: usize) -> BiomeId {
|
||||||
|
if x < 4 && y < self.height() / 4 && z < 4 {
|
||||||
|
BiomeId(self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4])
|
||||||
|
} else {
|
||||||
|
BiomeId::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_biome(&mut self, x: usize, y: usize, z: usize, b: BiomeId) {
|
||||||
|
if x < 4 && y < self.height() / 4 && z < 4 {
|
||||||
|
self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = b.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the chunk data packet with the given number of sections for this
|
||||||
|
/// chunk. This does not include unapplied changes.
|
||||||
|
pub(crate) fn chunk_data_packet(
|
||||||
|
&self,
|
||||||
|
pos: ChunkPos,
|
||||||
|
num_sections: usize,
|
||||||
|
) -> ChunkDataAndUpdateLight {
|
||||||
|
let mut blocks_and_biomes = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..num_sections {
|
||||||
|
match self.sections.get(i) {
|
||||||
|
Some(sect) => {
|
||||||
|
blocks_and_biomes.extend_from_slice(§.compact_data);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Extra chunk sections are encoded as empty with the default biome.
|
||||||
|
|
||||||
|
// non air block count
|
||||||
|
0i16.encode(&mut blocks_and_biomes).unwrap();
|
||||||
|
// blocks
|
||||||
|
encode_paletted_container_single(0, &mut blocks_and_biomes).unwrap();
|
||||||
|
// biomes
|
||||||
|
encode_paletted_container_single(0, &mut blocks_and_biomes).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let motion_blocking = if num_sections == self.sections.len() {
|
||||||
|
self.heightmap.clone()
|
||||||
|
} else {
|
||||||
|
// This is bad for two reasons:
|
||||||
|
// - Rebuilding the heightmap from scratch is slow.
|
||||||
|
// - The new heightmap is subtly wrong because we are building it from blocks
|
||||||
|
// with the modifications applied already.
|
||||||
|
// But you shouldn't be using chunks with heights different than the dimensions
|
||||||
|
// they're residing in anyway, so whatever.
|
||||||
|
let mut heightmap = Vec::new();
|
||||||
|
build_heightmap(&self.sections, &mut heightmap);
|
||||||
|
heightmap
|
||||||
|
};
|
||||||
|
|
||||||
|
ChunkDataAndUpdateLight {
|
||||||
|
chunk_x: pos.x,
|
||||||
|
chunk_z: pos.z,
|
||||||
|
heightmaps: Nbt(ChunkDataHeightmaps { motion_blocking }),
|
||||||
|
blocks_and_biomes,
|
||||||
|
block_entities: Vec::new(), // TODO
|
||||||
|
trust_edges: true,
|
||||||
|
sky_light_mask: bitvec![u64, _; 1; num_sections + 2],
|
||||||
|
block_light_mask: BitVec::new(),
|
||||||
|
empty_sky_light_mask: BitVec::new(),
|
||||||
|
empty_block_light_mask: BitVec::new(),
|
||||||
|
sky_light_arrays: vec![[0xff; 2048]; num_sections + 2],
|
||||||
|
block_light_arrays: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the unapplied changes to this chunk as a block change packet.
|
||||||
|
pub(crate) fn block_change_packet(&self, pos: ChunkPos) -> Option<BlockChangePacket> {
|
||||||
|
if !self.modified {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn apply_modifications(&mut self) {
|
||||||
|
if self.modified {
|
||||||
|
self.modified = false;
|
||||||
|
|
||||||
|
for sect in self.sections.iter_mut() {
|
||||||
|
if sect.modified {
|
||||||
|
sect.modified = false;
|
||||||
|
|
||||||
|
sect.compact_data.clear();
|
||||||
|
|
||||||
|
// TODO: consider cave_air and void_air.
|
||||||
|
let non_air_block_count = sect.blocks.iter().filter(|&&b| b != 0).count();
|
||||||
|
|
||||||
|
(non_air_block_count as i16)
|
||||||
|
.encode(&mut sect.compact_data)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
encode_paletted_container(§.blocks, 4, 9, 15, &mut sect.compact_data)
|
||||||
|
.unwrap();
|
||||||
|
// TODO: The direct bits per idx changes depending on the number of biomes in
|
||||||
|
// the biome registry.
|
||||||
|
encode_paletted_container(§.biomes, 0, 4, 6, &mut sect.compact_data)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build_heightmap(&self.sections, &mut self.heightmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum BlockChangePacket {
|
||||||
|
Single(BlockChange),
|
||||||
|
Multi(MultiBlockChange),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BlockChangePacket> for ClientPlayPacket {
|
||||||
|
fn from(p: BlockChangePacket) -> Self {
|
||||||
|
match p {
|
||||||
|
BlockChangePacket::Single(p) => p.into(),
|
||||||
|
BlockChangePacket::Multi(p) => p.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The X and Z position of a chunk in a world.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
|
pub struct ChunkPos {
|
||||||
|
/// The X position of the chunk.
|
||||||
|
pub x: i32,
|
||||||
|
/// The Z position of the chunk.
|
||||||
|
pub z: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkPos {
|
||||||
|
pub const fn new(x: i32, z: i32) -> Self {
|
||||||
|
Self { x, z }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the chunk position of the chunk that the given coordinates are
|
||||||
|
/// contained within.
|
||||||
|
pub fn from_xz(xz: DVec2) -> Self {
|
||||||
|
Self {
|
||||||
|
x: (xz.x / 16.0) as i32,
|
||||||
|
z: (xz.y / 16.0) as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(i32, i32)> for ChunkPos {
|
||||||
|
fn from((x, z): (i32, i32)) -> Self {
|
||||||
|
ChunkPos { x, z }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 16x16x16 section of blocks, biomes, and light in a chunk.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ChunkSection {
|
||||||
|
/// The blocks in this section, stored in x, z, y order.
|
||||||
|
blocks: [u16; 4096],
|
||||||
|
biomes: [u16; 64],
|
||||||
|
compact_data: Vec<u8>,
|
||||||
|
/// If the blocks or biomes were modified.
|
||||||
|
modified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the MOTION_BLOCKING heightmap.
|
||||||
|
fn build_heightmap(sections: &[ChunkSection], heightmap: &mut Vec<i64>) {
|
||||||
|
let height = sections.len() * 16;
|
||||||
|
let bits_per_val = log2_ceil(height);
|
||||||
|
let vals_per_u64 = 64 / bits_per_val;
|
||||||
|
let num_u64s = Integer::div_ceil(&256, &vals_per_u64);
|
||||||
|
|
||||||
|
heightmap.clear();
|
||||||
|
heightmap.resize(num_u64s, 0);
|
||||||
|
|
||||||
|
for x in 0..16 {
|
||||||
|
for z in 0..16 {
|
||||||
|
for y in (0..height).rev() {
|
||||||
|
// TODO: is_solid || is_fluid heuristic for motion blocking.
|
||||||
|
if sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] != 0 {
|
||||||
|
let column_height = y as u64;
|
||||||
|
|
||||||
|
let i = x * 16 + z; // TODO: X or Z major?
|
||||||
|
heightmap[i / vals_per_u64] |=
|
||||||
|
(column_height << (i % vals_per_u64 * bits_per_val)) as i64;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_paletted_container(
|
||||||
|
entries: &[u16],
|
||||||
|
min_bits_per_idx: usize,
|
||||||
|
direct_threshold: usize,
|
||||||
|
direct_bits_per_idx: usize,
|
||||||
|
w: &mut impl Write,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut palette = Vec::new();
|
||||||
|
|
||||||
|
for &entry in entries {
|
||||||
|
if !palette.contains(&entry) {
|
||||||
|
palette.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bits_per_idx = log2_ceil(palette.len());
|
||||||
|
|
||||||
|
(bits_per_idx as u8).encode(w)?;
|
||||||
|
|
||||||
|
if bits_per_idx == 0 {
|
||||||
|
// Single value case
|
||||||
|
debug_assert_eq!(palette.len(), 1);
|
||||||
|
VarInt(palette[0] as i32).encode(w)?;
|
||||||
|
VarInt(0).encode(w)?; // data array length
|
||||||
|
} else if bits_per_idx >= direct_threshold {
|
||||||
|
// Direct case
|
||||||
|
// Skip the palette
|
||||||
|
let idxs_per_u64 = 64 / direct_bits_per_idx;
|
||||||
|
let num_u64s = Integer::div_ceil(&entries.len(), &idxs_per_u64);
|
||||||
|
|
||||||
|
VarInt(num_u64s as i32).encode(w)?;
|
||||||
|
|
||||||
|
for &entry in entries {
|
||||||
|
let mut val = 0u64;
|
||||||
|
for i in 0..idxs_per_u64 {
|
||||||
|
val |= (entry as u64) << (i * direct_bits_per_idx);
|
||||||
|
}
|
||||||
|
val.encode(w)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Indirect case
|
||||||
|
VarInt(palette.len() as i32).encode(w)?;
|
||||||
|
for &val in &palette {
|
||||||
|
VarInt(val as i32).encode(w)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bits_per_idx = bits_per_idx.max(min_bits_per_idx);
|
||||||
|
let idxs_per_u64 = 64 / bits_per_idx;
|
||||||
|
let num_u64s = Integer::div_ceil(&entries.len(), &idxs_per_u64);
|
||||||
|
|
||||||
|
VarInt(num_u64s as i32).encode(w)?;
|
||||||
|
|
||||||
|
for &entry in entries {
|
||||||
|
let palette_idx = palette
|
||||||
|
.iter()
|
||||||
|
.position(|&e| e == entry)
|
||||||
|
.expect("entry should be in the palette") as u64;
|
||||||
|
|
||||||
|
let mut val = 0u64;
|
||||||
|
for i in 0..idxs_per_u64 {
|
||||||
|
val |= palette_idx << (i * bits_per_idx);
|
||||||
|
}
|
||||||
|
val.encode(w)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a paletted container where all values are the same.
|
||||||
|
fn encode_paletted_container_single(entry: u16, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
0u8.encode(w)?; // bits per idx
|
||||||
|
VarInt(entry as i32).encode(w)?; // single value
|
||||||
|
VarInt(0).encode(w)?; // data array length
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the log base 2 rounded up.
|
||||||
|
fn log2_ceil(n: usize) -> usize {
|
||||||
|
n.next_power_of_two().trailing_zeros() as usize
|
||||||
|
}
|
220
src/chunk_store.rs
Normal file
220
src/chunk_store.rs
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
|
||||||
|
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||||
|
|
||||||
|
use crate::component::{
|
||||||
|
ComponentStore, Components, ComponentsMut, Error, Id, IdData, IdRaw, ZippedComponents,
|
||||||
|
ZippedComponentsRaw,
|
||||||
|
};
|
||||||
|
use crate::Chunk;
|
||||||
|
|
||||||
|
pub struct ChunkStore {
|
||||||
|
comps: ComponentStore<ChunkId>,
|
||||||
|
chunks: RwLock<Vec<Chunk>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkStore {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
comps: ComponentStore::new(),
|
||||||
|
chunks: RwLock::new(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(&mut self, height: usize) -> ChunkId {
|
||||||
|
assert!(height % 16 == 0, "chunk height must be a multiple of 16");
|
||||||
|
|
||||||
|
let id = self.comps.create_item();
|
||||||
|
let chunk = Chunk::new(height / 16);
|
||||||
|
|
||||||
|
let idx = id.0.idx as usize;
|
||||||
|
if idx >= self.chunks.get_mut().len() {
|
||||||
|
self.chunks.get_mut().push(chunk);
|
||||||
|
} else {
|
||||||
|
self.chunks.get_mut()[idx] = chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&mut self, chunk: ChunkId) -> bool {
|
||||||
|
if self.comps.delete_item(chunk) {
|
||||||
|
let idx = chunk.0.idx as usize;
|
||||||
|
self.chunks.get_mut()[idx].deallocate();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.comps.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_valid(&self, chunk: ChunkId) -> bool {
|
||||||
|
self.comps.is_valid(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<Z>(&self, z: Z, chunk: ChunkId) -> Option<Z::Item>
|
||||||
|
where
|
||||||
|
Z: ZippedComponents<Id = ChunkId>,
|
||||||
|
{
|
||||||
|
self.comps.get(z, chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter<'a, Z>(&'a self, z: Z) -> impl FusedIterator<Item = (ChunkId, Z::Item)> + 'a
|
||||||
|
where
|
||||||
|
Z: ZippedComponents<Id = ChunkId> + 'a,
|
||||||
|
{
|
||||||
|
self.comps.iter(z)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn par_iter<'a, Z>(&'a self, z: Z) -> impl ParallelIterator<Item = (ChunkId, Z::Item)> + 'a
|
||||||
|
where
|
||||||
|
Z: ZippedComponents<Id = ChunkId> + 'a,
|
||||||
|
{
|
||||||
|
self.comps.par_iter(z)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ids(&self) -> impl FusedIterator<Item = ChunkId> + Clone + '_ {
|
||||||
|
self.comps.ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn par_ids(&self) -> impl ParallelIterator<Item = ChunkId> + Clone + '_ {
|
||||||
|
self.comps.par_ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chunks(&self) -> Result<Chunks, Error> {
|
||||||
|
Ok(Chunks {
|
||||||
|
chunks: self.chunks.try_read().ok_or(Error::NoReadAccess)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chunks_mut(&self) -> Result<ChunksMut, Error> {
|
||||||
|
Ok(ChunksMut {
|
||||||
|
chunks: self.chunks.try_write().ok_or(Error::NoWriteAccess)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_component<C: 'static + Send + Sync + Default>(&mut self) {
|
||||||
|
self.comps.register_component::<C>();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister_component<C: 'static + Send + Sync + Default>(&mut self) {
|
||||||
|
self.comps.unregister_component::<C>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_registered<C: 'static + Send + Sync + Default>(&self) -> bool {
|
||||||
|
self.comps.is_registered::<C>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn components<C: 'static + Send + Sync + Default>(
|
||||||
|
&self,
|
||||||
|
) -> Result<Components<C, ChunkId>, Error> {
|
||||||
|
self.comps.components::<C>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn components_mut<C: 'static + Send + Sync + Default>(
|
||||||
|
&self,
|
||||||
|
) -> Result<ComponentsMut<C, ChunkId>, Error> {
|
||||||
|
self.comps.components_mut::<C>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct ChunkId(pub(crate) IdData);
|
||||||
|
|
||||||
|
impl ChunkId {
|
||||||
|
/// The value of the default [`ChunkId`] which is always invalid.
|
||||||
|
pub const NULL: Self = Self(IdData::NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdRaw for ChunkId {
|
||||||
|
fn to_data(self) -> IdData {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_data(id: IdData) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Id for ChunkId {}
|
||||||
|
|
||||||
|
pub struct Chunks<'a> {
|
||||||
|
chunks: RwLockReadGuard<'a, Vec<Chunk>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b Chunks<'a> {
|
||||||
|
type RawItem = &'b Chunk;
|
||||||
|
type RawIter = std::slice::Iter<'b, Chunk>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, Chunk>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.chunks[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.chunks.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.chunks.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b Chunks<'a> {
|
||||||
|
type Id = ChunkId;
|
||||||
|
type Item = &'b Chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChunksMut<'a> {
|
||||||
|
chunks: RwLockWriteGuard<'a, Vec<Chunk>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b ChunksMut<'a> {
|
||||||
|
type RawItem = &'b Chunk;
|
||||||
|
type RawIter = std::slice::Iter<'b, Chunk>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, Chunk>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.chunks[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.chunks.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.chunks.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b ChunksMut<'a> {
|
||||||
|
type Id = ChunkId;
|
||||||
|
type Item = &'b Chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b mut ChunksMut<'a> {
|
||||||
|
type RawItem = &'b mut Chunk;
|
||||||
|
type RawIter = std::slice::IterMut<'b, Chunk>;
|
||||||
|
type RawParIter = rayon::slice::IterMut<'b, Chunk>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&mut self.chunks[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.chunks.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.chunks.par_iter_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b mut ChunksMut<'a> {
|
||||||
|
type Id = ChunkId;
|
||||||
|
type Item = &'b mut Chunk;
|
||||||
|
}
|
755
src/client.rs
Normal file
755
src/client.rs
Normal file
|
@ -0,0 +1,755 @@
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use flume::{Receiver, Sender, TrySendError};
|
||||||
|
use glm::DVec3;
|
||||||
|
|
||||||
|
use crate::block_pos::BlockPos;
|
||||||
|
use crate::chunk_store::ChunkId;
|
||||||
|
use crate::config::{
|
||||||
|
Biome, BiomeGrassColorModifier, BiomePrecipitation, Dimension, DimensionEffects, DimensionId,
|
||||||
|
};
|
||||||
|
pub use crate::packets::play::GameMode;
|
||||||
|
use crate::packets::play::{
|
||||||
|
Biome as BiomeRegistryBiome, BiomeAdditionsSound, BiomeEffects, BiomeMoodSound, BiomeMusic,
|
||||||
|
BiomeParticle, BiomeParticleOptions, BiomeProperty, BiomeRegistry, ClientPlayPacket,
|
||||||
|
DimensionCodec, DimensionType, DimensionTypeRegistry, DimensionTypeRegistryEntry, Disconnect,
|
||||||
|
JoinGame, KeepAliveClientbound, PlayerPositionAndLook, PlayerPositionAndLookFlags,
|
||||||
|
ServerPlayPacket, SpawnPosition, UnloadChunk, UpdateViewDistance, UpdateViewPosition,
|
||||||
|
};
|
||||||
|
use crate::protocol::{BoundedInt, Nbt};
|
||||||
|
use crate::server::ServerPacketChannels;
|
||||||
|
use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance};
|
||||||
|
use crate::var_int::VarInt;
|
||||||
|
use crate::world::WorldId;
|
||||||
|
use crate::{glm, ident, ChunkPos, EntityId, Server, SharedServer, Text, Ticks, LIBRARY_NAMESPACE};
|
||||||
|
|
||||||
|
pub struct MaybeClient(pub(crate) Option<Box<Client>>);
|
||||||
|
|
||||||
|
impl MaybeClient {
|
||||||
|
pub fn get(&self) -> Option<&Client> {
|
||||||
|
self.0.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self) -> Option<&mut Client> {
|
||||||
|
self.0.as_deref_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drops the inner [`Client`]. Future calls to [`get`](MaybeClient::get)
|
||||||
|
/// and [`get_mut`](MaybeClient::get_mut) will return `None`.
|
||||||
|
///
|
||||||
|
/// If the client was still connected prior to calling this function, the
|
||||||
|
/// client is disconnected from the server without a displayed reason.
|
||||||
|
///
|
||||||
|
/// If the inner client was already dropped, this function has no effect.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.0 = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_disconnected(&self) -> bool {
|
||||||
|
self.get().map_or(true, |c| c.is_disconnected())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a client connected to the server after logging in.
|
||||||
|
pub struct Client {
|
||||||
|
shared: SharedServer,
|
||||||
|
/// Setting this to `None` disconnects the client.
|
||||||
|
send: Option<Sender<ClientPlayPacket>>,
|
||||||
|
recv: Receiver<ServerPlayPacket>,
|
||||||
|
/// The tick this client was created.
|
||||||
|
created_tick: Ticks,
|
||||||
|
username: String,
|
||||||
|
on_ground: bool,
|
||||||
|
old_position: DVec3,
|
||||||
|
new_position: DVec3,
|
||||||
|
/// Measured in degrees
|
||||||
|
yaw: f32,
|
||||||
|
/// Measured in degrees
|
||||||
|
pitch: f32,
|
||||||
|
/// If any of position, yaw, or pitch were modified by the
|
||||||
|
/// user this tick.
|
||||||
|
teleported_this_tick: bool,
|
||||||
|
/// Counts up as teleports are made.
|
||||||
|
teleport_id_counter: u32,
|
||||||
|
/// The number of pending client teleports that have yet to receive a
|
||||||
|
/// confirmation. Inbound client position packets are ignored while this
|
||||||
|
/// is nonzero.
|
||||||
|
pending_teleports: u32,
|
||||||
|
spawn_position: BlockPos,
|
||||||
|
spawn_position_yaw: f32,
|
||||||
|
/// If spawn_position or spawn_position_yaw were modified this tick.
|
||||||
|
modified_spawn_position: bool,
|
||||||
|
/// The world that this client was in at the end of the previous tick.
|
||||||
|
old_world: WorldId,
|
||||||
|
new_world: WorldId,
|
||||||
|
events: Vec<Event>,
|
||||||
|
/// The ID of the last keepalive sent.
|
||||||
|
last_keepalive_id: i64,
|
||||||
|
/// If the last sent keepalive got a response.
|
||||||
|
got_keepalive: bool,
|
||||||
|
old_max_view_distance: u8,
|
||||||
|
new_max_view_distance: u8,
|
||||||
|
/// Entities that were visible to this client at the end of the last tick.
|
||||||
|
/// This is used to determine what entity create/destroy packets should be
|
||||||
|
/// sent.
|
||||||
|
loaded_entities: HashSet<EntityId>,
|
||||||
|
hidden_entities: HashSet<EntityId>,
|
||||||
|
/// Loaded chunks and their positions.
|
||||||
|
loaded_chunks: HashMap<ChunkPos, ChunkId>,
|
||||||
|
old_game_mode: GameMode,
|
||||||
|
new_game_mode: GameMode,
|
||||||
|
settings: Option<Settings>,
|
||||||
|
// TODO: latency
|
||||||
|
// TODO: time, weather
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub(crate) fn new(
|
||||||
|
packet_channels: ServerPacketChannels,
|
||||||
|
username: String,
|
||||||
|
server: &Server,
|
||||||
|
) -> Self {
|
||||||
|
let (send, recv) = packet_channels;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
shared: server.shared().clone(),
|
||||||
|
send: Some(send),
|
||||||
|
recv,
|
||||||
|
created_tick: server.current_tick(),
|
||||||
|
username,
|
||||||
|
on_ground: false,
|
||||||
|
old_position: DVec3::default(),
|
||||||
|
new_position: DVec3::default(),
|
||||||
|
yaw: 0.0,
|
||||||
|
pitch: 0.0,
|
||||||
|
teleported_this_tick: false,
|
||||||
|
teleport_id_counter: 0,
|
||||||
|
pending_teleports: 0,
|
||||||
|
spawn_position: BlockPos::default(),
|
||||||
|
spawn_position_yaw: 0.0,
|
||||||
|
modified_spawn_position: true,
|
||||||
|
new_world: WorldId::NULL,
|
||||||
|
old_world: WorldId::NULL,
|
||||||
|
events: Vec::new(),
|
||||||
|
last_keepalive_id: 0,
|
||||||
|
got_keepalive: true,
|
||||||
|
old_max_view_distance: 0,
|
||||||
|
new_max_view_distance: 16,
|
||||||
|
loaded_entities: HashSet::new(),
|
||||||
|
hidden_entities: HashSet::new(),
|
||||||
|
loaded_chunks: HashMap::new(),
|
||||||
|
old_game_mode: GameMode::Survival,
|
||||||
|
new_game_mode: GameMode::Survival,
|
||||||
|
settings: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn created_tick(&self) -> Ticks {
|
||||||
|
self.created_tick
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn username(&self) -> &str {
|
||||||
|
&self.username
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn position(&self) -> DVec3 {
|
||||||
|
self.new_position
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn yaw(&self) -> f32 {
|
||||||
|
self.yaw
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pitch(&self) -> f32 {
|
||||||
|
self.pitch
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn teleport(&mut self, pos: DVec3, yaw_degrees: f32, pitch_degrees: f32) {
|
||||||
|
self.new_position = pos;
|
||||||
|
self.yaw = yaw_degrees;
|
||||||
|
self.pitch = pitch_degrees;
|
||||||
|
|
||||||
|
if !self.teleported_this_tick {
|
||||||
|
self.teleported_this_tick = true;
|
||||||
|
|
||||||
|
self.pending_teleports = match self.pending_teleports.checked_add(1) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
self.disconnect("Too many pending teleports");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.teleport_id_counter = self.teleport_id_counter.wrapping_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_ground(&self) -> bool {
|
||||||
|
self.on_ground
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the point at which compasses point at.
|
||||||
|
pub fn set_spawn_position(&mut self, pos: impl Into<BlockPos>, yaw_degrees: f32) {
|
||||||
|
let pos = pos.into();
|
||||||
|
if pos != self.spawn_position || yaw_degrees != self.spawn_position_yaw {
|
||||||
|
self.spawn_position = pos;
|
||||||
|
self.spawn_position_yaw = yaw_degrees;
|
||||||
|
self.modified_spawn_position = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn world(&self) -> WorldId {
|
||||||
|
self.new_world
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_world(&mut self, new_world: WorldId) {
|
||||||
|
self.new_world = new_world;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to enqueue a play packet to be sent to this client. The client
|
||||||
|
/// is disconnected if the clientbound packet buffer is full.
|
||||||
|
pub(crate) fn send_packet(&mut self, packet: impl Into<ClientPlayPacket>) {
|
||||||
|
send_packet(&mut self.send, packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect(&mut self, reason: impl Into<Text>) {
|
||||||
|
if self.send.is_some() {
|
||||||
|
let txt = reason.into();
|
||||||
|
log::info!("disconnecting client '{}': \"{txt}\"", self.username);
|
||||||
|
|
||||||
|
self.send_packet(Disconnect { reason: txt });
|
||||||
|
|
||||||
|
self.send = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect_no_reason(&mut self) {
|
||||||
|
if self.send.is_some() {
|
||||||
|
log::info!("disconnecting client '{}' (no reason)", self.username);
|
||||||
|
self.send = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_disconnected(&self) -> bool {
|
||||||
|
self.send.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn events(&self) -> &[Event] {
|
||||||
|
&self.events
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_view_distance(&self) -> u8 {
|
||||||
|
self.new_max_view_distance
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The new view distance is clamped to `2..=32`.
|
||||||
|
pub fn set_max_view_distance(&mut self, dist: u8) {
|
||||||
|
self.new_max_view_distance = dist.clamp(2, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settings(&self) -> Option<&Settings> {
|
||||||
|
self.settings.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, client_eid: EntityId, server: &Server) {
|
||||||
|
self.events.clear();
|
||||||
|
|
||||||
|
if self.is_disconnected() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(0..self.recv.len()).for_each(|_| match self.recv.try_recv().unwrap() {
|
||||||
|
ServerPlayPacket::TeleportConfirm(p) => {
|
||||||
|
if self.pending_teleports == 0 {
|
||||||
|
self.disconnect("Unexpected teleport confirmation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let got = p.teleport_id.0 as u32;
|
||||||
|
let expected = self
|
||||||
|
.teleport_id_counter
|
||||||
|
.wrapping_sub(self.pending_teleports);
|
||||||
|
|
||||||
|
if got == expected {
|
||||||
|
self.pending_teleports -= 1;
|
||||||
|
} else {
|
||||||
|
self.disconnect(format!(
|
||||||
|
"Unexpected teleport ID (expected {expected}, got {got})"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServerPlayPacket::QueryBlockNbt(_) => {}
|
||||||
|
ServerPlayPacket::SetDifficulty(_) => {}
|
||||||
|
ServerPlayPacket::ChatMessageServerbound(_) => {}
|
||||||
|
ServerPlayPacket::ClientStatus(_) => {}
|
||||||
|
ServerPlayPacket::ClientSettings(p) => {
|
||||||
|
let old = self.settings.replace(Settings {
|
||||||
|
locale: p.locale.0,
|
||||||
|
view_distance: p.view_distance.0,
|
||||||
|
chat_mode: p.chat_mode,
|
||||||
|
chat_colors: p.chat_colors,
|
||||||
|
main_hand: p.main_hand,
|
||||||
|
displayed_skin_parts: p.displayed_skin_parts,
|
||||||
|
allow_server_listings: p.allow_server_listings,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.events.push(Event::SettingsChanged(old));
|
||||||
|
}
|
||||||
|
ServerPlayPacket::TabCompleteServerbound(_) => {}
|
||||||
|
ServerPlayPacket::ClickWindowButton(_) => {}
|
||||||
|
ServerPlayPacket::ClickWindow(_) => {}
|
||||||
|
ServerPlayPacket::CloseWindow(_) => {}
|
||||||
|
ServerPlayPacket::PluginMessageServerbound(_) => {}
|
||||||
|
ServerPlayPacket::EditBook(_) => {}
|
||||||
|
ServerPlayPacket::QueryEntityNbt(_) => {}
|
||||||
|
ServerPlayPacket::InteractEntity(_) => {}
|
||||||
|
ServerPlayPacket::GenerateStructure(_) => {}
|
||||||
|
ServerPlayPacket::KeepAliveServerbound(p) => {
|
||||||
|
if self.got_keepalive {
|
||||||
|
self.disconnect("Unexpected keepalive");
|
||||||
|
} else if p.id != self.last_keepalive_id {
|
||||||
|
self.disconnect(format!(
|
||||||
|
"Keepalive ids don't match (expected {}, got {})",
|
||||||
|
self.last_keepalive_id, p.id
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.got_keepalive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServerPlayPacket::LockDifficulty(_) => {}
|
||||||
|
ServerPlayPacket::PlayerPosition(p) => handle_movement_packet(
|
||||||
|
self,
|
||||||
|
glm::vec3(p.x, p.feet_y, p.z),
|
||||||
|
self.yaw,
|
||||||
|
self.pitch,
|
||||||
|
p.on_ground,
|
||||||
|
),
|
||||||
|
ServerPlayPacket::PlayerPositionAndRotation(p) => handle_movement_packet(
|
||||||
|
self,
|
||||||
|
glm::vec3(p.x, p.feet_y, p.z),
|
||||||
|
p.yaw,
|
||||||
|
p.pitch,
|
||||||
|
p.on_ground,
|
||||||
|
),
|
||||||
|
ServerPlayPacket::PlayerRotation(p) => {
|
||||||
|
handle_movement_packet(self, self.new_position, p.yaw, p.pitch, p.on_ground)
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerPlayPacket::PlayerMovement(p) => {
|
||||||
|
handle_movement_packet(self, self.new_position, self.yaw, self.pitch, p.on_ground)
|
||||||
|
}
|
||||||
|
ServerPlayPacket::VehicleMoveServerbound(_) => {}
|
||||||
|
ServerPlayPacket::SteerBoat(_) => {}
|
||||||
|
ServerPlayPacket::PickItem(_) => {}
|
||||||
|
ServerPlayPacket::CraftRecipeRequest(_) => {}
|
||||||
|
ServerPlayPacket::PlayerAbilitiesServerbound(_) => {}
|
||||||
|
ServerPlayPacket::PlayerDigging(_) => {}
|
||||||
|
ServerPlayPacket::EntityAction(_) => {}
|
||||||
|
ServerPlayPacket::SteerVehicle(_) => {}
|
||||||
|
ServerPlayPacket::Pong(_) => {}
|
||||||
|
ServerPlayPacket::SetRecipeBookState(_) => {}
|
||||||
|
ServerPlayPacket::SetDisplayedRecipe(_) => {}
|
||||||
|
ServerPlayPacket::NameItem(_) => {}
|
||||||
|
ServerPlayPacket::ResourcePackStatus(_) => {}
|
||||||
|
ServerPlayPacket::AdvancementTab(_) => {}
|
||||||
|
ServerPlayPacket::SelectTrade(_) => {}
|
||||||
|
ServerPlayPacket::SetBeaconEffect(_) => {}
|
||||||
|
ServerPlayPacket::HeldItemChangeServerbound(_) => {}
|
||||||
|
ServerPlayPacket::UpdateCommandBlock(_) => {}
|
||||||
|
ServerPlayPacket::UpdateCommandBlockMinecart(_) => {}
|
||||||
|
ServerPlayPacket::CreativeInventoryAction(_) => {}
|
||||||
|
ServerPlayPacket::UpdateJigsawBlock(_) => {}
|
||||||
|
ServerPlayPacket::UpdateStructureBlock(_) => {}
|
||||||
|
ServerPlayPacket::UpdateSign(_) => {}
|
||||||
|
ServerPlayPacket::PlayerArmSwing(_) => {}
|
||||||
|
ServerPlayPacket::Spectate(_) => {}
|
||||||
|
ServerPlayPacket::PlayerBlockPlacement(_) => {}
|
||||||
|
ServerPlayPacket::UseItem(_) => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
fn handle_movement_packet(
|
||||||
|
client: &mut Client,
|
||||||
|
new_position: DVec3,
|
||||||
|
new_yaw: f32,
|
||||||
|
new_pitch: f32,
|
||||||
|
new_on_ground: bool,
|
||||||
|
) {
|
||||||
|
if client.pending_teleports == 0 {
|
||||||
|
let event = Event::Movement {
|
||||||
|
position: client.new_position,
|
||||||
|
yaw_degrees: client.yaw,
|
||||||
|
pitch_degrees: client.pitch,
|
||||||
|
on_ground: client.on_ground,
|
||||||
|
};
|
||||||
|
|
||||||
|
client.new_position = new_position;
|
||||||
|
client.yaw = new_yaw;
|
||||||
|
client.pitch = new_pitch;
|
||||||
|
client.on_ground = new_on_ground;
|
||||||
|
|
||||||
|
client.events.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(send) = &self.send {
|
||||||
|
if send.is_disconnected() || self.recv.is_disconnected() {
|
||||||
|
self.send = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let worlds = server.worlds.worlds().unwrap();
|
||||||
|
let world = server.worlds.get(&worlds, self.new_world);
|
||||||
|
|
||||||
|
let dim_id = world.map_or(DimensionId::default(), |w| w.dimension());
|
||||||
|
let dim = server.dimension(dim_id);
|
||||||
|
|
||||||
|
if self.created_tick == server.current_tick() {
|
||||||
|
// Send the join game packet and other initial packets. We defer this until now
|
||||||
|
// so that the user can set the client's location, game mode, etc.
|
||||||
|
|
||||||
|
self.send_packet(JoinGame {
|
||||||
|
entity_id: client_eid.to_network_id(),
|
||||||
|
is_hardcore: false,
|
||||||
|
gamemode: self.new_game_mode,
|
||||||
|
previous_gamemode: self.old_game_mode,
|
||||||
|
dimension_names: server
|
||||||
|
.dimensions()
|
||||||
|
.map(|(_, id)| ident!("{LIBRARY_NAMESPACE}:dimension_{}", id.0))
|
||||||
|
.collect(),
|
||||||
|
dimension_codec: Nbt(make_dimension_codec(server)),
|
||||||
|
dimension: Nbt(to_dimension_registry_item(dim)),
|
||||||
|
dimension_name: ident!("{LIBRARY_NAMESPACE}:dimension_{}", dim_id.0),
|
||||||
|
hashed_seed: 0,
|
||||||
|
max_players: VarInt(0),
|
||||||
|
view_distance: BoundedInt(VarInt(self.new_max_view_distance as i32)),
|
||||||
|
simulation_distance: VarInt(16),
|
||||||
|
reduced_debug_info: false,
|
||||||
|
enable_respawn_screen: false, // TODO
|
||||||
|
is_debug: false,
|
||||||
|
is_flat: false, // TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
self.teleport(self.position(), self.yaw(), self.pitch());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.modified_spawn_position {
|
||||||
|
self.modified_spawn_position = false;
|
||||||
|
|
||||||
|
self.send_packet(SpawnPosition {
|
||||||
|
location: self.spawn_position,
|
||||||
|
angle: self.spawn_position_yaw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update view distance fog on the client if necessary.
|
||||||
|
if self.old_max_view_distance != self.new_max_view_distance {
|
||||||
|
self.old_max_view_distance = self.new_max_view_distance;
|
||||||
|
if self.created_tick != server.current_tick() {
|
||||||
|
self.send_packet(UpdateViewDistance {
|
||||||
|
view_distance: BoundedInt(VarInt(self.new_max_view_distance as i32)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's time to send another keepalive.
|
||||||
|
if server.last_keepalive == server.tick_start() {
|
||||||
|
if self.got_keepalive {
|
||||||
|
let id = rand::random();
|
||||||
|
self.send_packet(KeepAliveClientbound { id });
|
||||||
|
self.last_keepalive_id = id;
|
||||||
|
self.got_keepalive = false;
|
||||||
|
} else {
|
||||||
|
self.disconnect("Timed out (no keepalive response)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load, update, and unload chunks.
|
||||||
|
if self.old_world != self.new_world {
|
||||||
|
let old_dim = server
|
||||||
|
.worlds
|
||||||
|
.get(&worlds, self.old_world)
|
||||||
|
.map_or(DimensionId::default(), |w| w.dimension());
|
||||||
|
|
||||||
|
let new_dim = dim_id;
|
||||||
|
|
||||||
|
if old_dim != new_dim {
|
||||||
|
// Changing dimensions automatically unloads all chunks and
|
||||||
|
// entities.
|
||||||
|
self.loaded_chunks.clear();
|
||||||
|
self.loaded_entities.clear();
|
||||||
|
|
||||||
|
todo!("need to send respawn packet for new dimension");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.old_world = self.new_world;
|
||||||
|
}
|
||||||
|
|
||||||
|
let view_dist = self
|
||||||
|
.settings
|
||||||
|
.as_ref()
|
||||||
|
.map_or(2, |s| s.view_distance)
|
||||||
|
.min(self.new_max_view_distance);
|
||||||
|
|
||||||
|
let chunks = server.chunks.chunks().unwrap();
|
||||||
|
|
||||||
|
let center = ChunkPos::from_xz(self.new_position.xz());
|
||||||
|
|
||||||
|
// Send the update view position packet if the client changes the chunk section
|
||||||
|
// they're in.
|
||||||
|
{
|
||||||
|
let old_section = self.old_position.map(|n| (n / 16.0) as i32);
|
||||||
|
let new_section = self.new_position.map(|n| (n / 16.0) as i32);
|
||||||
|
|
||||||
|
if old_section != new_section {
|
||||||
|
self.send_packet(UpdateViewPosition {
|
||||||
|
chunk_x: VarInt(new_section.x),
|
||||||
|
chunk_z: VarInt(new_section.z),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unload deleted chunks and those outside the view distance. Also update
|
||||||
|
// existing chunks.
|
||||||
|
self.loaded_chunks.retain(|&pos, &mut chunk_id| {
|
||||||
|
if let Some(chunk) = server.chunks.get(&chunks, chunk_id) {
|
||||||
|
// The cache stops chunk data packets from needing to be sent when a player is
|
||||||
|
// jumping between adjacent chunks.
|
||||||
|
let cache = 2;
|
||||||
|
if is_chunk_in_view_distance(center, pos, view_dist + cache) {
|
||||||
|
if let Some(pkt) = chunk.block_change_packet(pos) {
|
||||||
|
send_packet(&mut self.send, pkt);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
send_packet(
|
||||||
|
&mut self.send,
|
||||||
|
UnloadChunk {
|
||||||
|
chunk_x: pos.x,
|
||||||
|
chunk_z: pos.z,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
send_packet(
|
||||||
|
&mut self.send,
|
||||||
|
UnloadChunk {
|
||||||
|
chunk_x: pos.x,
|
||||||
|
chunk_z: pos.z,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load new chunks within the view distance
|
||||||
|
for pos in chunks_in_view_distance(center, view_dist) {
|
||||||
|
if let Entry::Vacant(ve) = self.loaded_chunks.entry(pos) {
|
||||||
|
if let Some(&chunk_id) = world.and_then(|w| w.chunks().get(&pos)) {
|
||||||
|
if let Some(chunk) = server.chunks.get(&chunks, chunk_id) {
|
||||||
|
ve.insert(chunk_id);
|
||||||
|
self.send_packet(chunk.chunk_data_packet(pos, (dim.height / 16) as usize));
|
||||||
|
if let Some(pkt) = chunk.block_change_packet(pos) {
|
||||||
|
self.send_packet(pkt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is done after the chunks are loaded so that the "downloading terrain"
|
||||||
|
// screen is closed at the appropriate time.
|
||||||
|
if self.teleported_this_tick {
|
||||||
|
self.teleported_this_tick = false;
|
||||||
|
|
||||||
|
self.send_packet(PlayerPositionAndLook {
|
||||||
|
x: self.new_position.x,
|
||||||
|
y: self.new_position.y,
|
||||||
|
z: self.new_position.z,
|
||||||
|
yaw: self.yaw,
|
||||||
|
pitch: self.pitch,
|
||||||
|
flags: PlayerPositionAndLookFlags::new(false, false, false, false, false),
|
||||||
|
teleport_id: VarInt((self.teleport_id_counter - 1) as i32),
|
||||||
|
dismount_vehicle: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.old_position = self.new_position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Client {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
log::trace!("Dropping client '{}'", self.username);
|
||||||
|
self.shared.dec_client_count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
/// Settings were changed. The value in this variant is the previous client
|
||||||
|
/// settings.
|
||||||
|
SettingsChanged(Option<Settings>),
|
||||||
|
|
||||||
|
/// The client has moved. The values in this variant are the previous
|
||||||
|
/// position and look.
|
||||||
|
Movement {
|
||||||
|
position: DVec3,
|
||||||
|
yaw_degrees: f32,
|
||||||
|
pitch_degrees: f32,
|
||||||
|
on_ground: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Settings {
|
||||||
|
/// e.g. en_US
|
||||||
|
pub locale: String,
|
||||||
|
/// The client side render distance, in chunks.
|
||||||
|
///
|
||||||
|
/// The value is always in `2..=32`.
|
||||||
|
pub view_distance: u8,
|
||||||
|
pub chat_mode: ChatMode,
|
||||||
|
/// `true` if the client has chat colors enabled, `false` otherwise.
|
||||||
|
pub chat_colors: bool,
|
||||||
|
pub main_hand: MainHand,
|
||||||
|
pub displayed_skin_parts: DisplayedSkinParts,
|
||||||
|
pub allow_server_listings: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use crate::packets::play::{ChatMode, DisplayedSkinParts, MainHand};
|
||||||
|
|
||||||
|
fn send_packet(send_opt: &mut Option<Sender<ClientPlayPacket>>, pkt: impl Into<ClientPlayPacket>) {
|
||||||
|
if let Some(send) = send_opt {
|
||||||
|
match send.try_send(pkt.into()) {
|
||||||
|
Err(TrySendError::Full(_)) => {
|
||||||
|
log::warn!("max outbound packet capacity reached for client");
|
||||||
|
*send_opt = None;
|
||||||
|
}
|
||||||
|
Err(TrySendError::Disconnected(_)) => {
|
||||||
|
*send_opt = None;
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_dimension_codec(server: &Server) -> DimensionCodec {
|
||||||
|
let mut dims = Vec::new();
|
||||||
|
for (dim, id) in server.dimensions() {
|
||||||
|
let id = id.0 as i32;
|
||||||
|
dims.push(DimensionTypeRegistryEntry {
|
||||||
|
name: ident!("{LIBRARY_NAMESPACE}:dimension_type_{id}"),
|
||||||
|
id,
|
||||||
|
element: to_dimension_registry_item(dim),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut biomes = Vec::new();
|
||||||
|
for (biome, id) in server.biomes() {
|
||||||
|
biomes.push(to_biome_registry_item(biome, id.0 as i32));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The client needs a biome named "minecraft:plains" in the registry to
|
||||||
|
// connect. This is probably a bug.
|
||||||
|
//
|
||||||
|
// If the issue is resolved, just delete this block.
|
||||||
|
if !biomes.iter().any(|b| b.name == ident!("plains")) {
|
||||||
|
let biome = Biome::default();
|
||||||
|
assert_eq!(biome.name, ident!("plains"));
|
||||||
|
biomes.push(to_biome_registry_item(&biome, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
DimensionCodec {
|
||||||
|
dimension_type_registry: DimensionTypeRegistry {
|
||||||
|
typ: ident!("dimension_type"),
|
||||||
|
value: dims,
|
||||||
|
},
|
||||||
|
biome_registry: BiomeRegistry {
|
||||||
|
typ: ident!("worldgen/biome"),
|
||||||
|
value: biomes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_dimension_registry_item(dim: &Dimension) -> DimensionType {
|
||||||
|
DimensionType {
|
||||||
|
piglin_safe: true,
|
||||||
|
natural: dim.natural,
|
||||||
|
ambient_light: dim.ambient_light,
|
||||||
|
fixed_time: dim.fixed_time.map(|t| t as i64),
|
||||||
|
infiniburn: "#minecraft:infiniburn_overworld".into(),
|
||||||
|
respawn_anchor_works: true,
|
||||||
|
has_skylight: true,
|
||||||
|
bed_works: true,
|
||||||
|
effects: match dim.effects {
|
||||||
|
DimensionEffects::Overworld => ident!("overworld"),
|
||||||
|
DimensionEffects::TheNether => ident!("the_nether"),
|
||||||
|
DimensionEffects::TheEnd => ident!("the_end"),
|
||||||
|
},
|
||||||
|
has_raids: true,
|
||||||
|
min_y: dim.min_y,
|
||||||
|
height: dim.height,
|
||||||
|
logical_height: dim.height,
|
||||||
|
coordinate_scale: 1.0,
|
||||||
|
ultrawarm: false,
|
||||||
|
has_ceiling: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_biome_registry_item(biome: &Biome, id: i32) -> BiomeRegistryBiome {
|
||||||
|
BiomeRegistryBiome {
|
||||||
|
name: biome.name.clone(),
|
||||||
|
id,
|
||||||
|
element: BiomeProperty {
|
||||||
|
precipitation: match biome.precipitation {
|
||||||
|
BiomePrecipitation::Rain => "rain",
|
||||||
|
BiomePrecipitation::Snow => "snow",
|
||||||
|
BiomePrecipitation::None => "none",
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
depth: 0.125,
|
||||||
|
temperature: 0.8,
|
||||||
|
scale: 0.05,
|
||||||
|
downfall: 0.4,
|
||||||
|
category: "none".into(),
|
||||||
|
temperature_modifier: None,
|
||||||
|
effects: BiomeEffects {
|
||||||
|
sky_color: biome.sky_color as i32,
|
||||||
|
water_fog_color: biome.water_fog_color as i32,
|
||||||
|
fog_color: biome.fog_color as i32,
|
||||||
|
water_color: biome.water_color as i32,
|
||||||
|
foliage_color: biome.foliage_color.map(|x| x as i32),
|
||||||
|
grass_color: None,
|
||||||
|
grass_color_modifier: match biome.grass_color_modifier {
|
||||||
|
BiomeGrassColorModifier::Swamp => Some("swamp".into()),
|
||||||
|
BiomeGrassColorModifier::DarkForest => Some("dark_forest".into()),
|
||||||
|
BiomeGrassColorModifier::None => None,
|
||||||
|
},
|
||||||
|
music: biome.music.as_ref().map(|bm| BiomeMusic {
|
||||||
|
replace_current_music: bm.replace_current_music,
|
||||||
|
sound: bm.sound.clone(),
|
||||||
|
max_delay: bm.max_delay,
|
||||||
|
min_delay: bm.min_delay,
|
||||||
|
}),
|
||||||
|
ambient_sound: biome.ambient_sound.clone(),
|
||||||
|
additions_sound: biome.additions_sound.as_ref().map(|a| BiomeAdditionsSound {
|
||||||
|
sound: a.sound.clone(),
|
||||||
|
tick_chance: a.tick_chance,
|
||||||
|
}),
|
||||||
|
mood_sound: biome.mood_sound.as_ref().map(|m| BiomeMoodSound {
|
||||||
|
sound: m.sound.clone(),
|
||||||
|
tick_delay: m.tick_delay,
|
||||||
|
offset: m.offset,
|
||||||
|
block_search_extent: m.block_search_extent,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
particle: biome.particle.as_ref().map(|p| BiomeParticle {
|
||||||
|
probability: p.probability,
|
||||||
|
options: BiomeParticleOptions { typ: p.typ.clone() },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
307
src/codec.rs
Normal file
307
src/codec.rs
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
use std::io::Read;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use aes::Aes128;
|
||||||
|
use anyhow::{bail, ensure, Context};
|
||||||
|
use cfb8::cipher::{AsyncStreamCipher, NewCipher};
|
||||||
|
use cfb8::Cfb8;
|
||||||
|
use flate2::bufread::{ZlibDecoder, ZlibEncoder};
|
||||||
|
use flate2::Compression;
|
||||||
|
use log::{log_enabled, Level};
|
||||||
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
use super::packets::{DecodePacket, EncodePacket};
|
||||||
|
use crate::protocol::{Decode, Encode, MAX_PACKET_SIZE};
|
||||||
|
use crate::var_int::VarInt;
|
||||||
|
|
||||||
|
pub struct Encoder<W> {
|
||||||
|
write: W,
|
||||||
|
buf: Vec<u8>,
|
||||||
|
compress_buf: Vec<u8>,
|
||||||
|
compression_threshold: Option<u32>,
|
||||||
|
cipher: Option<Cipher>,
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: AsyncWrite + Unpin> Encoder<W> {
|
||||||
|
pub fn new(write: W, timeout: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
write,
|
||||||
|
buf: Vec::new(),
|
||||||
|
compress_buf: Vec::new(),
|
||||||
|
compression_threshold: None,
|
||||||
|
cipher: None,
|
||||||
|
timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_packet(&mut self, packet: &impl EncodePacket) -> anyhow::Result<()> {
|
||||||
|
timeout(self.timeout, self.write_packet_impl(packet)).await?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_packet_impl(&mut self, packet: &impl EncodePacket) -> anyhow::Result<()> {
|
||||||
|
self.buf.clear();
|
||||||
|
|
||||||
|
packet.encode_packet(&mut self.buf)?;
|
||||||
|
|
||||||
|
let data_len = self.buf.len();
|
||||||
|
|
||||||
|
ensure!(data_len <= i32::MAX as usize, "bad packet data length");
|
||||||
|
|
||||||
|
if let Some(threshold) = self.compression_threshold {
|
||||||
|
if data_len >= threshold as usize {
|
||||||
|
let mut z = ZlibEncoder::new(self.buf.as_slice(), Compression::best());
|
||||||
|
|
||||||
|
self.compress_buf.clear();
|
||||||
|
z.read_to_end(&mut self.compress_buf)?;
|
||||||
|
|
||||||
|
let data_len_len = VarInt(data_len as i32).written_size();
|
||||||
|
let packet_len = data_len_len + self.compress_buf.len();
|
||||||
|
|
||||||
|
ensure!(packet_len <= MAX_PACKET_SIZE as usize, "bad packet length");
|
||||||
|
|
||||||
|
self.buf.clear();
|
||||||
|
VarInt(packet_len as i32).encode(&mut self.buf)?;
|
||||||
|
VarInt(data_len as i32).encode(&mut self.buf)?;
|
||||||
|
self.buf.extend_from_slice(&self.compress_buf);
|
||||||
|
} else {
|
||||||
|
let packet_len = VarInt(0).written_size() + data_len;
|
||||||
|
|
||||||
|
ensure!(packet_len <= MAX_PACKET_SIZE as usize, "bad packet length");
|
||||||
|
|
||||||
|
self.buf.clear();
|
||||||
|
VarInt(packet_len as i32).encode(&mut self.buf)?;
|
||||||
|
VarInt(0).encode(&mut self.buf)?;
|
||||||
|
packet.encode_packet(&mut self.buf)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let packet_len = data_len;
|
||||||
|
|
||||||
|
ensure!(packet_len <= MAX_PACKET_SIZE as usize, "bad packet length");
|
||||||
|
|
||||||
|
self.buf.clear();
|
||||||
|
VarInt(packet_len as i32).encode(&mut self.buf)?;
|
||||||
|
packet.encode_packet(&mut self.buf)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cipher) = &mut self.cipher {
|
||||||
|
cipher.encrypt(&mut self.buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.write.write_all(&self.buf).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_encryption(&mut self, key: &[u8; 16]) {
|
||||||
|
self.cipher = Some(NewCipher::new(key.into(), key.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_compression(&mut self, threshold: u32) {
|
||||||
|
self.compression_threshold = Some(threshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Decoder<R> {
|
||||||
|
read: R,
|
||||||
|
buf: Vec<u8>,
|
||||||
|
decompress_buf: Vec<u8>,
|
||||||
|
compression_threshold: Option<u32>,
|
||||||
|
cipher: Option<Cipher>,
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: AsyncRead + Unpin> Decoder<R> {
|
||||||
|
pub fn new(read: R, timeout: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
read,
|
||||||
|
buf: Vec::new(),
|
||||||
|
decompress_buf: Vec::new(),
|
||||||
|
compression_threshold: None,
|
||||||
|
cipher: None,
|
||||||
|
timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_packet<P: DecodePacket>(&mut self) -> anyhow::Result<P> {
|
||||||
|
timeout(self.timeout, self.read_packet_impl()).await?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_packet_impl<P: DecodePacket>(&mut self) -> anyhow::Result<P> {
|
||||||
|
let packet_len = self
|
||||||
|
.read_var_int_async()
|
||||||
|
.await
|
||||||
|
.context("reading packet length")?;
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
(0..=MAX_PACKET_SIZE).contains(&packet_len),
|
||||||
|
"invalid packet length of {packet_len}."
|
||||||
|
);
|
||||||
|
|
||||||
|
self.buf.resize(packet_len as usize, 0);
|
||||||
|
|
||||||
|
self.read
|
||||||
|
.read_exact(&mut self.buf)
|
||||||
|
.await
|
||||||
|
.context("reading packet body")?;
|
||||||
|
|
||||||
|
if let Some(cipher) = &mut self.cipher {
|
||||||
|
cipher.decrypt(&mut self.buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut packet_contents = self.buf.as_slice();
|
||||||
|
|
||||||
|
// Compression enabled?
|
||||||
|
let packet = if self.compression_threshold.is_some() {
|
||||||
|
// The length of the packet data once uncompressed (zero indicates no
|
||||||
|
// compression).
|
||||||
|
let data_len = VarInt::decode(&mut packet_contents)
|
||||||
|
.context("reading data length (once uncompressed)")?
|
||||||
|
.0;
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
(0..=MAX_PACKET_SIZE).contains(&data_len),
|
||||||
|
"invalid packet data length of {data_len}."
|
||||||
|
);
|
||||||
|
|
||||||
|
if data_len != 0 {
|
||||||
|
let mut z = ZlibDecoder::new(&mut packet_contents);
|
||||||
|
self.decompress_buf.resize(data_len as usize, 0);
|
||||||
|
z.read_exact(&mut self.decompress_buf)
|
||||||
|
.context("uncompressing packet body")?;
|
||||||
|
|
||||||
|
let mut uncompressed = self.decompress_buf.as_slice();
|
||||||
|
let packet = P::decode_packet(&mut uncompressed)
|
||||||
|
.context("decoding packet after uncompressing")?;
|
||||||
|
ensure!(
|
||||||
|
uncompressed.is_empty(),
|
||||||
|
"packet contents were not read completely"
|
||||||
|
);
|
||||||
|
packet
|
||||||
|
} else {
|
||||||
|
P::decode_packet(&mut packet_contents).context("decoding packet")?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
P::decode_packet(&mut packet_contents).context("decoding packet")?
|
||||||
|
};
|
||||||
|
|
||||||
|
if !packet_contents.is_empty() {
|
||||||
|
if log_enabled!(Level::Debug) {
|
||||||
|
log::debug!("complete packet after partial decode: {packet:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!(
|
||||||
|
"packet contents were not decoded completely ({} bytes remaining)",
|
||||||
|
packet_contents.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_var_int_async(&mut self) -> anyhow::Result<i32> {
|
||||||
|
let mut val = 0;
|
||||||
|
for i in 0..VarInt::MAX_SIZE {
|
||||||
|
let array = &mut [self.read.read_u8().await?];
|
||||||
|
if let Some(cipher) = &mut self.cipher {
|
||||||
|
cipher.decrypt(array);
|
||||||
|
}
|
||||||
|
let [byte] = *array;
|
||||||
|
|
||||||
|
val |= (byte as i32 & 0b01111111) << (i * 7);
|
||||||
|
if byte & 0b10000000 == 0 {
|
||||||
|
return Ok(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bail!("var int is too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_encryption(&mut self, key: &[u8; 16]) {
|
||||||
|
self.cipher = Some(NewCipher::new(key.into(), key.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_compression(&mut self, threshold: u32) {
|
||||||
|
self.compression_threshold = Some(threshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The AES block cipher with a 128 bit key, using the CFB-8 mode of
|
||||||
|
/// operation.
|
||||||
|
type Cipher = Cfb8<Aes128>;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::packets::test::TestPacket;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn encode_decode() {
|
||||||
|
encode_decode_impl().await
|
||||||
|
}
|
||||||
|
|
||||||
|
const CRYPT_KEY: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
|
||||||
|
const TIMEOUT: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
|
async fn encode_decode_impl() {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let t = tokio::spawn(listen(tx));
|
||||||
|
|
||||||
|
let stream = TcpStream::connect(rx.await.unwrap()).await.unwrap();
|
||||||
|
let mut encoder = Encoder::new(stream, TIMEOUT);
|
||||||
|
|
||||||
|
send_test_packet(&mut encoder).await;
|
||||||
|
encoder.enable_compression(10);
|
||||||
|
send_test_packet(&mut encoder).await;
|
||||||
|
encoder.enable_encryption(&CRYPT_KEY);
|
||||||
|
send_test_packet(&mut encoder).await;
|
||||||
|
send_test_packet(&mut encoder).await;
|
||||||
|
send_test_packet(&mut encoder).await;
|
||||||
|
|
||||||
|
t.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn listen(local_addr: oneshot::Sender<SocketAddr>) {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
|
||||||
|
local_addr.send(listener.local_addr().unwrap()).unwrap();
|
||||||
|
|
||||||
|
let stream = listener.accept().await.unwrap().0;
|
||||||
|
let mut decoder = Decoder::new(stream, TIMEOUT);
|
||||||
|
|
||||||
|
recv_test_packet(&mut decoder).await;
|
||||||
|
decoder.enable_compression(10);
|
||||||
|
recv_test_packet(&mut decoder).await;
|
||||||
|
decoder.enable_encryption(&CRYPT_KEY);
|
||||||
|
recv_test_packet(&mut decoder).await;
|
||||||
|
recv_test_packet(&mut decoder).await;
|
||||||
|
recv_test_packet(&mut decoder).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_test_packet(w: &mut Encoder<TcpStream>) {
|
||||||
|
w.write_packet(&TestPacket {
|
||||||
|
first: "abcdefghijklmnopqrstuvwxyz".to_string().into(),
|
||||||
|
second: vec![0x1234, 0xabcd],
|
||||||
|
third: 0x1122334455667788,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recv_test_packet(r: &mut Decoder<TcpStream>) {
|
||||||
|
let TestPacket {
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
third,
|
||||||
|
} = r.read_packet().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(&first, "abcdefghijklmnopqrstuvwxyz");
|
||||||
|
assert_eq!(&second, &[0x1234, 0xabcd]);
|
||||||
|
assert_eq!(third, 0x1122334455667788);
|
||||||
|
}
|
||||||
|
}
|
550
src/component.rs
Normal file
550
src/component.rs
Normal file
|
@ -0,0 +1,550 @@
|
||||||
|
use std::any::{Any, TypeId};
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
|
||||||
|
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
use rayon::iter::{
|
||||||
|
IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator,
|
||||||
|
IntoParallelRefMutIterator, ParallelIterator,
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Contains custom components
|
||||||
|
pub(crate) struct ComponentStore<I: Id> {
|
||||||
|
ids: Vec<Slot>,
|
||||||
|
next_free_head: u32,
|
||||||
|
count: u32,
|
||||||
|
components: HashMap<TypeId, Box<dyn ComponentVec>>,
|
||||||
|
_marker: PhantomData<fn(I) -> I>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: Id> ComponentStore<I> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
ids: Vec::new(),
|
||||||
|
next_free_head: 0,
|
||||||
|
count: 0,
|
||||||
|
components: HashMap::new(),
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.count as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_item(&mut self) -> I {
|
||||||
|
assert!(self.count < u32::MAX - 1, "too many items");
|
||||||
|
|
||||||
|
if self.next_free_head == self.ids.len() as u32 {
|
||||||
|
self.ids.push(Slot {
|
||||||
|
gen: ONE,
|
||||||
|
next_free: None,
|
||||||
|
});
|
||||||
|
self.count += 1;
|
||||||
|
self.next_free_head += 1;
|
||||||
|
for v in self.components.values_mut() {
|
||||||
|
v.push_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
I::from_data(IdData {
|
||||||
|
idx: self.next_free_head - 1,
|
||||||
|
gen: ONE,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let s = &mut self.ids[self.next_free_head as usize];
|
||||||
|
s.gen = match NonZeroU32::new(s.gen.get().wrapping_add(1)) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
log::warn!("generation overflow at idx = {}", self.next_free_head);
|
||||||
|
ONE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let next_free = s.next_free.expect("corrupt free list");
|
||||||
|
let id = I::from_data(IdData {
|
||||||
|
idx: self.next_free_head,
|
||||||
|
gen: s.gen,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.next_free_head = next_free;
|
||||||
|
self.count += 1;
|
||||||
|
s.next_free = None;
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_item(&mut self, id: I) -> bool {
|
||||||
|
let id = id.to_data();
|
||||||
|
match self.ids.get_mut(id.idx as usize) {
|
||||||
|
Some(Slot {
|
||||||
|
gen,
|
||||||
|
next_free: nf @ None,
|
||||||
|
}) if *gen == id.gen => {
|
||||||
|
*nf = Some(self.next_free_head);
|
||||||
|
self.next_free_head = id.idx;
|
||||||
|
self.count -= 1;
|
||||||
|
|
||||||
|
for vec in self.components.values_mut() {
|
||||||
|
vec.clear_at(id.idx as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_valid(&self, id: I) -> bool {
|
||||||
|
let id = id.to_data();
|
||||||
|
match self.ids.get(id.idx as usize) {
|
||||||
|
Some(Slot {
|
||||||
|
gen,
|
||||||
|
next_free: None,
|
||||||
|
}) => *gen == id.gen,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<Z: ZippedComponents<Id = I>>(&self, z: Z, id: I) -> Option<Z::Item> {
|
||||||
|
if self.is_valid(id) {
|
||||||
|
Some(z.raw_get(id.to_data().idx as usize))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter<'a, Z: ZippedComponents<Id = I> + 'a>(
|
||||||
|
&'a self,
|
||||||
|
z: Z,
|
||||||
|
) -> impl FusedIterator<Item = (I, Z::Item)> + 'a {
|
||||||
|
self.ids
|
||||||
|
.iter()
|
||||||
|
.zip(z.raw_iter())
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, (s, c))| {
|
||||||
|
if s.next_free.is_none() {
|
||||||
|
Some((
|
||||||
|
I::from_data(IdData {
|
||||||
|
idx: i as u32,
|
||||||
|
gen: s.gen,
|
||||||
|
}),
|
||||||
|
c,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn par_iter<'a, Z: ZippedComponents<Id = I> + 'a>(
|
||||||
|
&'a self,
|
||||||
|
z: Z,
|
||||||
|
) -> impl ParallelIterator<Item = (I, Z::Item)> + 'a {
|
||||||
|
self.ids
|
||||||
|
.par_iter()
|
||||||
|
.zip(z.raw_par_iter())
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, (s, c))| {
|
||||||
|
if s.next_free.is_none() {
|
||||||
|
Some((
|
||||||
|
I::from_data(IdData {
|
||||||
|
idx: i as u32,
|
||||||
|
gen: s.gen,
|
||||||
|
}),
|
||||||
|
c,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ids(&self) -> impl FusedIterator<Item = I> + Clone + '_ {
|
||||||
|
self.ids.iter().enumerate().filter_map(|(i, s)| {
|
||||||
|
if s.next_free.is_none() {
|
||||||
|
Some(I::from_data(IdData {
|
||||||
|
idx: i as u32,
|
||||||
|
gen: s.gen,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn par_ids(&self) -> impl ParallelIterator<Item = I> + Clone + '_ {
|
||||||
|
self.ids.par_iter().enumerate().filter_map(|(i, s)| {
|
||||||
|
if s.next_free.is_none() {
|
||||||
|
Some(I::from_data(IdData {
|
||||||
|
idx: i as u32,
|
||||||
|
gen: s.gen,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_component<C: 'static + Send + Sync + Default>(&mut self) {
|
||||||
|
if let Entry::Vacant(ve) = self.components.entry(TypeId::of::<C>()) {
|
||||||
|
let mut vec = Vec::new();
|
||||||
|
vec.resize_with(self.ids.len(), C::default);
|
||||||
|
ve.insert(Box::new(RwLock::new(vec)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister_component<C: 'static + Send + Sync + Default>(&mut self) {
|
||||||
|
self.components.remove(&TypeId::of::<C>());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_registered<C: 'static + Send + Sync + Default>(&self) -> bool {
|
||||||
|
self.components.contains_key(&TypeId::of::<C>())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn components<C: 'static + Send + Sync + Default>(
|
||||||
|
&self,
|
||||||
|
) -> Result<Components<C, I>, Error> {
|
||||||
|
let handle = self
|
||||||
|
.components
|
||||||
|
.get(&TypeId::of::<C>())
|
||||||
|
.ok_or(Error::UnknownComponent)?
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<RwLock<Vec<C>>>()
|
||||||
|
.unwrap()
|
||||||
|
.try_read()
|
||||||
|
.ok_or(Error::NoReadAccess)?;
|
||||||
|
|
||||||
|
Ok(Components {
|
||||||
|
handle,
|
||||||
|
_marker: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn components_mut<C: 'static + Send + Sync + Default>(
|
||||||
|
&self,
|
||||||
|
) -> Result<ComponentsMut<C, I>, Error> {
|
||||||
|
let handle = self
|
||||||
|
.components
|
||||||
|
.get(&TypeId::of::<C>())
|
||||||
|
.ok_or(Error::UnknownComponent)?
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<RwLock<Vec<C>>>()
|
||||||
|
.unwrap()
|
||||||
|
.try_write()
|
||||||
|
.ok_or(Error::NoWriteAccess)?;
|
||||||
|
|
||||||
|
Ok(ComponentsMut {
|
||||||
|
handle,
|
||||||
|
_marker: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct Slot {
|
||||||
|
gen: NonZeroU32,
|
||||||
|
next_free: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Id: IdRaw + Copy + Send + Sync {}
|
||||||
|
|
||||||
|
const ONE: NonZeroU32 = match NonZeroU32::new(1) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
trait ComponentVec: Any + Send + Sync {
|
||||||
|
fn push_default(&mut self);
|
||||||
|
|
||||||
|
fn clear_at(&mut self, idx: usize);
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static + Send + Sync + Default> ComponentVec for RwLock<Vec<T>> {
|
||||||
|
fn push_default(&mut self) {
|
||||||
|
self.get_mut().push(T::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_at(&mut self, idx: usize) {
|
||||||
|
self.get_mut()[idx] = T::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self as _
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self as _
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Components<'a, C: 'static + Send + Sync + Default, I: Id> {
|
||||||
|
handle: RwLockReadGuard<'a, Vec<C>>,
|
||||||
|
_marker: PhantomData<fn(I) -> I>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, C: 'static + Send + Sync + Default, I: Id> ZippedComponentsRaw
|
||||||
|
for &'b Components<'a, C, I>
|
||||||
|
{
|
||||||
|
type RawItem = &'b C;
|
||||||
|
type RawIter = std::slice::Iter<'b, C>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, C>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.handle[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.handle.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.handle.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, C: 'static + Send + Sync + Default, I: Id> ZippedComponents
|
||||||
|
for &'b Components<'a, C, I>
|
||||||
|
{
|
||||||
|
type Id = I;
|
||||||
|
type Item = &'b C;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ComponentsMut<'a, C: 'static + Send + Sync + Default, I: Id> {
|
||||||
|
handle: RwLockWriteGuard<'a, Vec<C>>,
|
||||||
|
_marker: PhantomData<fn(I) -> I>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, C: 'static + Send + Sync + Default, I: Id> ZippedComponentsRaw
|
||||||
|
for &'b ComponentsMut<'a, C, I>
|
||||||
|
{
|
||||||
|
type RawItem = &'b C;
|
||||||
|
type RawIter = std::slice::Iter<'b, C>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, C>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.handle[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.handle.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.handle.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, C: 'static + Send + Sync + Default, I: Id> ZippedComponents
|
||||||
|
for &'b ComponentsMut<'a, C, I>
|
||||||
|
{
|
||||||
|
type Id = I;
|
||||||
|
type Item = &'b C;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, C: 'static + Send + Sync + Default, I: Id> ZippedComponentsRaw
|
||||||
|
for &'b mut ComponentsMut<'a, C, I>
|
||||||
|
{
|
||||||
|
type RawItem = &'b mut C;
|
||||||
|
type RawIter = std::slice::IterMut<'b, C>;
|
||||||
|
type RawParIter = rayon::slice::IterMut<'b, C>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&mut self.handle[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.handle.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.handle.par_iter_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, C: 'static + Send + Sync + Default, I: Id> ZippedComponents
|
||||||
|
for &'b mut ComponentsMut<'a, C, I>
|
||||||
|
{
|
||||||
|
type Id = I;
|
||||||
|
type Item = &'b mut C;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("an unknown component type was requested")]
|
||||||
|
UnknownComponent,
|
||||||
|
#[error("shared access to a component was requested while exclusive access was already held")]
|
||||||
|
NoReadAccess,
|
||||||
|
#[error(
|
||||||
|
"exclusive access to a component was requested while shared or exclusive access was \
|
||||||
|
already held"
|
||||||
|
)]
|
||||||
|
NoWriteAccess,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) mod private {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub trait ZippedComponentsRaw {
|
||||||
|
type RawItem: Send + Sync;
|
||||||
|
type RawIter: FusedIterator<Item = Self::RawItem>;
|
||||||
|
type RawParIter: IndexedParallelIterator<Item = Self::RawItem>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem;
|
||||||
|
fn raw_iter(self) -> Self::RawIter;
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IdRaw {
|
||||||
|
fn to_data(self) -> IdData;
|
||||||
|
fn from_data(id: IdData) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
|
pub struct IdData {
|
||||||
|
pub idx: u32,
|
||||||
|
pub gen: NonZeroU32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdData {
|
||||||
|
pub const NULL: IdData = IdData {
|
||||||
|
idx: u32::MAX,
|
||||||
|
gen: match NonZeroU32::new(u32::MAX) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => unreachable!(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for IdData {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::NULL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MultiZip<T> {
|
||||||
|
pub tuple: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> MultiZip<T> {
|
||||||
|
pub fn new(tuple: T) -> Self {
|
||||||
|
Self { tuple }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use private::*;
|
||||||
|
|
||||||
|
pub trait ZippedComponents: ZippedComponentsRaw<RawItem = Self::Item> {
|
||||||
|
type Id: Copy;
|
||||||
|
type Item: Send + Sync;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! tuple_impl {
|
||||||
|
($($T:ident),*) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
impl<$($T: ZippedComponentsRaw,)*> ZippedComponentsRaw for ($($T,)*) {
|
||||||
|
type RawItem = ($($T::RawItem,)*);
|
||||||
|
type RawIter = MultiZip<($($T::RawIter,)*)>;
|
||||||
|
type RawParIter = rayon::iter::MultiZip<($($T::RawParIter,)*)>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
let ($($T,)*) = self;
|
||||||
|
($($T.raw_get(idx),)*)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
let ($($T,)*) = self;
|
||||||
|
MultiZip::new(($($T.raw_iter(),)*))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
let ($($T,)*) = self;
|
||||||
|
($($T.raw_par_iter(),)*).into_par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
impl<$($T: Iterator,)*> Iterator for MultiZip<($($T,)*)> {
|
||||||
|
type Item = ($($T::Item,)*);
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let ($($T,)*) = &mut self.tuple;
|
||||||
|
Some(($($T.next()?,)*))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let lower = usize::MAX;
|
||||||
|
let upper: Option<usize> = None;
|
||||||
|
let ($($T,)*) = &self.tuple;
|
||||||
|
$(
|
||||||
|
let (l, u) = $T.size_hint();
|
||||||
|
let lower = lower.min(l);
|
||||||
|
let upper = match (upper, u) {
|
||||||
|
(Some(l), Some(r)) => Some(l.min(r)),
|
||||||
|
(Some(l), None) => Some(l),
|
||||||
|
(None, Some(r)) => Some(r),
|
||||||
|
(None, None) => None
|
||||||
|
};
|
||||||
|
)*
|
||||||
|
(lower, upper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
impl<$($T: ExactSizeIterator,)*> ExactSizeIterator for MultiZip<($($T,)*)> {
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
let len = usize::MAX;
|
||||||
|
let ($($T,)*) = &self.tuple;
|
||||||
|
$(
|
||||||
|
let len = len.min($T.len());
|
||||||
|
)*
|
||||||
|
|
||||||
|
debug_assert_eq!(self.size_hint(), (len, Some(len)));
|
||||||
|
len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
impl<$($T: DoubleEndedIterator + ExactSizeIterator,)*> DoubleEndedIterator for MultiZip<($($T,)*)> {
|
||||||
|
fn next_back(&mut self) -> Option<Self::Item> {
|
||||||
|
let len = self.len();
|
||||||
|
let ($($T,)*) = &mut self.tuple;
|
||||||
|
|
||||||
|
$(
|
||||||
|
let this_len = $T.len();
|
||||||
|
debug_assert!(this_len >= len);
|
||||||
|
for _ in 0..this_len - len {
|
||||||
|
$T.next_back();
|
||||||
|
}
|
||||||
|
let $T = $T.next_back();
|
||||||
|
)*
|
||||||
|
|
||||||
|
Some(($($T?,)*))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<$($T: FusedIterator,)*> FusedIterator for MultiZip<($($T,)*)> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tuple_impl!(A);
|
||||||
|
tuple_impl!(A, B);
|
||||||
|
tuple_impl!(A, B, C);
|
||||||
|
tuple_impl!(A, B, C, D);
|
||||||
|
tuple_impl!(A, B, C, D, E);
|
||||||
|
tuple_impl!(A, B, C, D, E, F);
|
||||||
|
tuple_impl!(A, B, C, D, E, F, G);
|
||||||
|
tuple_impl!(A, B, C, D, E, F, G, H);
|
||||||
|
tuple_impl!(A, B, C, D, E, F, G, H, I);
|
||||||
|
tuple_impl!(A, B, C, D, E, F, G, H, I, J);
|
||||||
|
tuple_impl!(A, B, C, D, E, F, G, H, I, J, K);
|
||||||
|
tuple_impl!(A, B, C, D, E, F, G, H, I, J, K, L);
|
522
src/config.rs
Normal file
522
src/config.rs
Normal file
|
@ -0,0 +1,522 @@
|
||||||
|
// TODO: rate limit, view distance?
|
||||||
|
|
||||||
|
use std::any::Any;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::ensure;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use tokio::runtime::Handle as TokioHandle;
|
||||||
|
|
||||||
|
use crate::server::{start_server, ShutdownError};
|
||||||
|
use crate::{ident, Identifier, NewClientData, Server, SharedServer, ShutdownResult, Text};
|
||||||
|
|
||||||
|
/// A builder type used to configure and start the server.
|
||||||
|
pub struct ServerConfig {
|
||||||
|
pub(crate) handler: Option<Box<dyn Handler>>,
|
||||||
|
pub(crate) address: SocketAddr,
|
||||||
|
pub(crate) update_duration: Duration,
|
||||||
|
pub(crate) online_mode: bool,
|
||||||
|
pub(crate) max_clients: usize,
|
||||||
|
pub(crate) clientbound_packet_capacity: usize,
|
||||||
|
pub(crate) serverbound_packet_capacity: usize,
|
||||||
|
pub(crate) tokio_handle: Option<TokioHandle>,
|
||||||
|
pub(crate) dimensions: Vec<Dimension>,
|
||||||
|
pub(crate) biomes: Vec<Biome>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerConfig {
|
||||||
|
/// Constructs a new server configuration with the provided handler.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
handler: None,
|
||||||
|
address: SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 25565).into(),
|
||||||
|
update_duration: Duration::from_secs_f64(1.0 / 20.0),
|
||||||
|
online_mode: false,
|
||||||
|
max_clients: 32,
|
||||||
|
clientbound_packet_capacity: 128,
|
||||||
|
serverbound_packet_capacity: 32,
|
||||||
|
tokio_handle: None,
|
||||||
|
dimensions: Vec::new(),
|
||||||
|
biomes: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Handler`] to use for this server.
|
||||||
|
pub fn handler(&mut self, handler: impl Handler) {
|
||||||
|
self.handler = Some(Box::new(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the socket address that the server will be bound to.
|
||||||
|
///
|
||||||
|
/// The default is `127.0.0.1:25565`.
|
||||||
|
pub fn address(&mut self, addr: impl Into<SocketAddr>) {
|
||||||
|
self.address = addr.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the duration of each game update.
|
||||||
|
///
|
||||||
|
/// On each game update (a.k.a. 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 the full update
|
||||||
|
/// duration has passed.
|
||||||
|
///
|
||||||
|
/// If the server is running behind schedule due to heavy load or some other
|
||||||
|
/// reason, the actual duration of a game update will exceed what has been
|
||||||
|
/// specified.
|
||||||
|
///
|
||||||
|
/// The duration must be nonzero.
|
||||||
|
///
|
||||||
|
/// The default value is the same as Minecraft's official server (20 ticks
|
||||||
|
/// per second). You may want to use a shorter duration if you can afford to
|
||||||
|
/// do so.
|
||||||
|
pub fn update_duration(&mut self, duration: Duration) {
|
||||||
|
self.update_duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the state of "online mode", which determines if client
|
||||||
|
/// authentication and encryption should occur.
|
||||||
|
///
|
||||||
|
/// When online mode is disabled, malicious clients can give themselves any
|
||||||
|
/// username and UUID they want, potentially gaining privileges they
|
||||||
|
/// might not otherwise have. Additionally, encryption is only enabled in
|
||||||
|
/// online mode. For these reasons online mode should only be disabled
|
||||||
|
/// for development purposes and enabled on servers exposed to the
|
||||||
|
/// internet.
|
||||||
|
///
|
||||||
|
/// By default, online mode is enabled.
|
||||||
|
pub fn online_mode(&mut self, online_mode: bool) {
|
||||||
|
self.online_mode = online_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum number of clients (past the login stage) allowed on the
|
||||||
|
/// server simultaneously.
|
||||||
|
///
|
||||||
|
/// The default is 32.
|
||||||
|
pub fn max_clients(&mut self, clients: usize) {
|
||||||
|
self.max_clients = clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The capacity of the buffer used to hold clientbound packets.
|
||||||
|
///
|
||||||
|
/// A larger capcity reduces the chance of packet loss but increases
|
||||||
|
/// potential memory usage. The default value is unspecified but should be
|
||||||
|
/// adequate for most situations.
|
||||||
|
///
|
||||||
|
/// The capacity must be nonzero.
|
||||||
|
pub fn clientbound_packet_capacity(&mut self, cap: usize) {
|
||||||
|
self.clientbound_packet_capacity = cap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the capacity of the buffer used to hold serverbound packets.
|
||||||
|
///
|
||||||
|
/// A larger capcity reduces the chance of packet loss but increases
|
||||||
|
/// potential memory usage. The default value is unspecified but should be
|
||||||
|
/// adequate for most situations.
|
||||||
|
///
|
||||||
|
/// The capacity must be nonzero.
|
||||||
|
pub fn serverbound_packet_capacity(&mut self, cap: usize) {
|
||||||
|
self.serverbound_packet_capacity = cap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the handle to the tokio runtime the server will use.
|
||||||
|
///
|
||||||
|
/// If a handle is not provided, the server will create its own tokio
|
||||||
|
/// runtime.
|
||||||
|
pub fn tokio_handle(&mut self, handle: TokioHandle) {
|
||||||
|
self.tokio_handle = Some(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new dimension to the server which is identified by the returned
|
||||||
|
/// [`DimensionId`]. The default dimension is added if none are provided.
|
||||||
|
///
|
||||||
|
/// Additionally, the documented requirements on the fields of [`Dimension`]
|
||||||
|
/// must be met. No more than `u16::MAX` dimensions may be added.
|
||||||
|
pub fn push_dimension(&mut self, dimension: Dimension) -> DimensionId {
|
||||||
|
let id = self.biomes.len();
|
||||||
|
self.dimensions.push(dimension);
|
||||||
|
DimensionId(id as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new biome to the server which is identified by the returned
|
||||||
|
/// [`BiomeId`]. The default biome is added if none are provided.
|
||||||
|
///
|
||||||
|
/// Additionally, the documented requirements on the fields of [`Biome`]
|
||||||
|
/// must be met. No more than `u16::MAX` biomes may be added.
|
||||||
|
pub fn push_biome(&mut self, biome: Biome) -> BiomeId {
|
||||||
|
let id = self.biomes.len();
|
||||||
|
self.biomes.push(biome);
|
||||||
|
BiomeId(id as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes the configuration and starts the server.
|
||||||
|
///
|
||||||
|
/// The function returns once the server has been shut down, a runtime error
|
||||||
|
/// occurs, or the configuration is invalid.
|
||||||
|
pub fn start(mut self) -> ShutdownResult {
|
||||||
|
if self.biomes.is_empty() {
|
||||||
|
self.biomes.push(Biome::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.dimensions.is_empty() {
|
||||||
|
self.dimensions.push(Dimension::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.validate().map_err(ShutdownError::from)?;
|
||||||
|
start_server(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> anyhow::Result<()> {
|
||||||
|
ensure!(
|
||||||
|
self.dimensions.len() <= u16::MAX as usize,
|
||||||
|
"more than u16::MAX dimensions added"
|
||||||
|
);
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
self.biomes.len() <= u16::MAX as usize,
|
||||||
|
"more than u16::MAX biomes added"
|
||||||
|
);
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
self.update_duration != Duration::ZERO,
|
||||||
|
"update duration must be nonzero"
|
||||||
|
);
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
self.clientbound_packet_capacity > 0,
|
||||||
|
"clientbound packet capacity must be nonzero"
|
||||||
|
);
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
self.serverbound_packet_capacity > 0,
|
||||||
|
"serverbound packet capacity must be nonzero"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (i, dim) in self.dimensions.iter().enumerate() {
|
||||||
|
ensure!(
|
||||||
|
dim.min_y % 16 == 0 && (-2032..=2016).contains(&dim.min_y),
|
||||||
|
"invalid min_y in dimension #{i}",
|
||||||
|
);
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
dim.height % 16 == 0
|
||||||
|
&& (0..=4064).contains(&dim.height)
|
||||||
|
&& dim.min_y.saturating_add(dim.height) <= 2032,
|
||||||
|
"invalid height in dimension #{i}",
|
||||||
|
);
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
(0.0..=1.0).contains(&dim.ambient_light),
|
||||||
|
"ambient_light is out of range in dimension #{i}",
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(fixed_time) = dim.fixed_time {
|
||||||
|
assert!(
|
||||||
|
(0..=24_000).contains(&fixed_time),
|
||||||
|
"fixed_time is out of range in dimension #{i}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut names = HashSet::new();
|
||||||
|
|
||||||
|
for biome in self.biomes.iter() {
|
||||||
|
ensure!(
|
||||||
|
names.insert(biome.name.clone()),
|
||||||
|
"biome \"{}\" already added",
|
||||||
|
biome.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ServerConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait containing callbacks which are invoked by the running Minecraft
|
||||||
|
/// server.
|
||||||
|
///
|
||||||
|
/// The handler is used from multiple threads and must therefore implement
|
||||||
|
/// `Send` and `Sync`. From within a single thread, callbacks are never invoked
|
||||||
|
/// recursively. In other words, a mutex can be aquired at the beginning of a
|
||||||
|
/// callback and released at the end without risk of deadlocking.
|
||||||
|
///
|
||||||
|
/// All methods are called from within a tokio context.
|
||||||
|
#[async_trait]
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
pub trait Handler: Any + Send + Sync {
|
||||||
|
/// Called after the server is created, but prior to accepting connections
|
||||||
|
/// and entering the update loop.
|
||||||
|
///
|
||||||
|
/// This is useful for performing initialization work with a guarantee that
|
||||||
|
/// no connections to the server will be made until this function returns.
|
||||||
|
///
|
||||||
|
/// # Default Implementation
|
||||||
|
/// The default implementation does nothing.
|
||||||
|
fn init(&self, server: &mut Server) {}
|
||||||
|
|
||||||
|
/// Called once at the beginning of every server update (also known as
|
||||||
|
/// a "tick").
|
||||||
|
///
|
||||||
|
/// The frequency of server updates can be configured by `update_duration`
|
||||||
|
/// in [`ServerConfig`].
|
||||||
|
///
|
||||||
|
/// # Default Implementation
|
||||||
|
/// The default implementation does nothing.
|
||||||
|
fn update(&self, server: &mut Server) {}
|
||||||
|
|
||||||
|
/// Called when the server receives a Server List Ping query.
|
||||||
|
/// Data for the query can be provided or the query can be ignored.
|
||||||
|
///
|
||||||
|
/// # Default Implementation
|
||||||
|
/// A placeholder response is returned.
|
||||||
|
async fn server_list_ping(
|
||||||
|
&self,
|
||||||
|
server: &SharedServer,
|
||||||
|
remote_addr: SocketAddr,
|
||||||
|
) -> ServerListPing {
|
||||||
|
ServerListPing::Respond {
|
||||||
|
online_players: server.client_count() as i32,
|
||||||
|
max_players: server.max_clients() as i32,
|
||||||
|
description: "A Minecraft Server".into(),
|
||||||
|
favicon_png: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when a client is disconnected due to the server being full.
|
||||||
|
/// The return value is the disconnect message to use.
|
||||||
|
///
|
||||||
|
/// # Default Implementation
|
||||||
|
/// A placeholder message is returned.
|
||||||
|
async fn max_client_message(&self, server: &SharedServer, npd: &NewClientData) -> Text {
|
||||||
|
// TODO: Standard translated text for this purpose?
|
||||||
|
"The server is full!".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called asynchronously for each client after successful authentication
|
||||||
|
/// (if online mode is enabled) to determine if they are allowed to join the
|
||||||
|
/// server. On success, a client-backed entity is spawned.
|
||||||
|
///
|
||||||
|
/// This function is the appropriate place to perform
|
||||||
|
/// whitelist checks, database queries, etc.
|
||||||
|
///
|
||||||
|
/// # Default Implementation
|
||||||
|
/// The client is allowed to join unconditionally.
|
||||||
|
async fn login(&self, server: &SharedServer, ncd: &NewClientData) -> Login {
|
||||||
|
Login::Join
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of the [`server_list_ping`](Handler::server_list_ping) callback.
|
||||||
|
pub enum ServerListPing {
|
||||||
|
/// Responds to the server list ping with the given information.
|
||||||
|
Respond {
|
||||||
|
online_players: i32,
|
||||||
|
max_players: i32,
|
||||||
|
description: Text,
|
||||||
|
/// The server's icon as the bytes of a PNG image.
|
||||||
|
/// The image must be 64x64 pixels.
|
||||||
|
///
|
||||||
|
/// No icon is used if the value is `None`.
|
||||||
|
favicon_png: Option<Arc<[u8]>>,
|
||||||
|
},
|
||||||
|
/// Ignores the query and disconnects from the client.
|
||||||
|
Ignore,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of the [`login`](Handler::login) callback.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Login {
|
||||||
|
/// The client may join the server.
|
||||||
|
Join,
|
||||||
|
/// The client may not join the server and will be disconnected with the
|
||||||
|
/// provided reason.
|
||||||
|
Disconnect(Text),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifies a particular [`Dimension`].
|
||||||
|
///
|
||||||
|
/// Dimension IDs are always valid and are cheap to copy and store.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Debug)]
|
||||||
|
pub struct DimensionId(pub(crate) u16);
|
||||||
|
|
||||||
|
/// Contains the configuration for a custom dimension type.
|
||||||
|
///
|
||||||
|
/// In Minecraft, "dimension" and "dimension type" are two different concepts.
|
||||||
|
/// For instance, the Overworld and Nether are dimensions, each with
|
||||||
|
/// their own dimension type. A dimension in this library is analogous to a
|
||||||
|
/// [`World`](crate::World) while the [`Dimension`] struct represents a
|
||||||
|
/// dimension type.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Dimension {
|
||||||
|
/// When false, compases will spin randomly.
|
||||||
|
pub natural: bool,
|
||||||
|
/// Must be between 0.0 and 1.0.
|
||||||
|
pub ambient_light: f32,
|
||||||
|
/// Must be between 0 and 24000.
|
||||||
|
pub fixed_time: Option<u16>,
|
||||||
|
/// Determines what skybox/fog effects to use.
|
||||||
|
pub effects: DimensionEffects,
|
||||||
|
/// The minimum height in which blocks can exist in this dimension.
|
||||||
|
///
|
||||||
|
/// `min_y` must meet the following conditions:
|
||||||
|
/// * `min_y % 16 == 0`
|
||||||
|
/// * `-2032 <= min_y <= 2016`
|
||||||
|
pub min_y: i32,
|
||||||
|
/// The total height in which blocks can exist in this dimension.
|
||||||
|
///
|
||||||
|
/// `height` must meet the following conditions:
|
||||||
|
/// * `height % 16 == 0`
|
||||||
|
/// * `0 <= height <= 4064`
|
||||||
|
/// * `min_y + height <= 2032`
|
||||||
|
pub height: i32,
|
||||||
|
// TODO: The following fields should be added if they can affect the
|
||||||
|
// appearance of the dimension to clients.
|
||||||
|
// * infiniburn
|
||||||
|
// * respawn_anchor_works
|
||||||
|
// * has_skylight
|
||||||
|
// * bed_works
|
||||||
|
// * has_raids
|
||||||
|
// * logical_height
|
||||||
|
// * coordinate_scale
|
||||||
|
// * ultrawarm
|
||||||
|
// * has_ceiling
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Dimension {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
natural: true,
|
||||||
|
ambient_light: 0.0,
|
||||||
|
fixed_time: None,
|
||||||
|
effects: DimensionEffects::Overworld,
|
||||||
|
min_y: -64,
|
||||||
|
height: 384,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum DimensionEffects {
|
||||||
|
Overworld,
|
||||||
|
TheNether,
|
||||||
|
TheEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifies a particular [`Biome`].
|
||||||
|
///
|
||||||
|
/// Biome IDs are always valid and are cheap to copy and store.
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
|
pub struct BiomeId(pub(crate) u16);
|
||||||
|
|
||||||
|
/// Contains the configuration for a custom biome.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Biome {
|
||||||
|
/// The unique name for this biome. The name can be
|
||||||
|
/// seen in the F3 debug menu.
|
||||||
|
pub name: Identifier,
|
||||||
|
pub precipitation: BiomePrecipitation,
|
||||||
|
pub sky_color: u32,
|
||||||
|
pub water_fog_color: u32,
|
||||||
|
pub fog_color: u32,
|
||||||
|
pub water_color: u32,
|
||||||
|
pub foliage_color: Option<u32>,
|
||||||
|
pub grass_color_modifier: BiomeGrassColorModifier,
|
||||||
|
pub music: Option<BiomeMusic>,
|
||||||
|
pub ambient_sound: Option<Identifier>,
|
||||||
|
pub additions_sound: Option<BiomeAdditionsSound>,
|
||||||
|
pub mood_sound: Option<BiomeMoodSound>,
|
||||||
|
pub particle: Option<BiomeParticle>,
|
||||||
|
// TODO: The following fields should be added if they can affect the appearance of the biome to
|
||||||
|
// clients.
|
||||||
|
// * depth: f32
|
||||||
|
// * temperature: f32
|
||||||
|
// * scale: f32
|
||||||
|
// * downfall: f32
|
||||||
|
// * category
|
||||||
|
// * temperature_modifier
|
||||||
|
// * grass_color (misleading name?)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Biome {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
name: ident!("plains"),
|
||||||
|
precipitation: BiomePrecipitation::Rain,
|
||||||
|
sky_color: 7907327,
|
||||||
|
water_fog_color: 329011,
|
||||||
|
fog_color: 12638463,
|
||||||
|
water_color: 4159204,
|
||||||
|
foliage_color: None,
|
||||||
|
grass_color_modifier: BiomeGrassColorModifier::None,
|
||||||
|
music: None,
|
||||||
|
ambient_sound: None,
|
||||||
|
additions_sound: None,
|
||||||
|
mood_sound: None,
|
||||||
|
particle: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum BiomePrecipitation {
|
||||||
|
Rain,
|
||||||
|
Snow,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BiomePrecipitation {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Rain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minecraft handles grass colors for swamps and dark oak forests in a special
|
||||||
|
/// way.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum BiomeGrassColorModifier {
|
||||||
|
Swamp,
|
||||||
|
DarkForest,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BiomeGrassColorModifier {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BiomeMusic {
|
||||||
|
pub replace_current_music: bool,
|
||||||
|
pub sound: Identifier,
|
||||||
|
pub min_delay: i32,
|
||||||
|
pub max_delay: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BiomeAdditionsSound {
|
||||||
|
pub sound: Identifier,
|
||||||
|
pub tick_chance: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BiomeMoodSound {
|
||||||
|
pub sound: Identifier,
|
||||||
|
pub tick_delay: i32,
|
||||||
|
pub offset: f64,
|
||||||
|
pub block_search_extent: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BiomeParticle {
|
||||||
|
pub probability: f32,
|
||||||
|
pub typ: Identifier,
|
||||||
|
}
|
641
src/entity.rs
Normal file
641
src/entity.rs
Normal file
|
@ -0,0 +1,641 @@
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
|
||||||
|
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::chunk::ChunkPos;
|
||||||
|
use crate::client::MaybeClient;
|
||||||
|
use crate::component::{
|
||||||
|
ComponentStore, Components, ComponentsMut, Error, Id, IdData, IdRaw, ZippedComponents,
|
||||||
|
ZippedComponentsRaw,
|
||||||
|
};
|
||||||
|
use crate::{Aabb, WorldId};
|
||||||
|
|
||||||
|
pub mod appearance;
|
||||||
|
|
||||||
|
pub use appearance::Appearance;
|
||||||
|
|
||||||
|
pub struct EntityStore {
|
||||||
|
comps: ComponentStore<EntityId>,
|
||||||
|
uuids: Vec<Uuid>,
|
||||||
|
clients: RwLock<Vec<MaybeClient>>,
|
||||||
|
appearances: RwLock<Vec<Appearance>>,
|
||||||
|
old_appearances: Vec<Appearance>,
|
||||||
|
uuid_to_entity: HashMap<Uuid, EntityId>,
|
||||||
|
/// Maps chunk positions to the set of all entities with bounding volumes
|
||||||
|
/// intersecting that chunk.
|
||||||
|
partition: HashMap<(WorldId, ChunkPos), Vec<EntityId>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntityStore {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
comps: ComponentStore::new(),
|
||||||
|
uuids: Vec::new(),
|
||||||
|
clients: RwLock::new(Vec::new()),
|
||||||
|
appearances: RwLock::new(Vec::new()),
|
||||||
|
old_appearances: Vec::new(),
|
||||||
|
uuid_to_entity: HashMap::new(),
|
||||||
|
partition: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the [`EntityId`] of the entity with the given UUID in an efficient
|
||||||
|
/// manner.
|
||||||
|
///
|
||||||
|
/// Returns `None` if there is no entity with the provided UUID. Returns
|
||||||
|
/// `Some` otherwise.
|
||||||
|
pub fn with_uuid(&self, uuid: Uuid) -> Option<EntityId> {
|
||||||
|
self.uuid_to_entity.get(&uuid).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns a new entity with the provided appearance. The new entity's
|
||||||
|
/// [`EntityId`] is returned.
|
||||||
|
pub fn create(&mut self, appearance: impl Into<Appearance>) -> EntityId {
|
||||||
|
let app = appearance.into();
|
||||||
|
loop {
|
||||||
|
let uuid = Uuid::from_bytes(rand::random());
|
||||||
|
if let Some(e) = self.create_with_uuid(app.clone(), uuid) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`create`](Entities::create), but requires specifying the new
|
||||||
|
/// entity's UUID. This is useful for deserialization.
|
||||||
|
///
|
||||||
|
/// The provided UUID must not conflict with an existing entity UUID. If it
|
||||||
|
/// does, `None` is returned and the entity is not spawned.
|
||||||
|
pub fn create_with_uuid(
|
||||||
|
&mut self,
|
||||||
|
appearance: impl Into<Appearance>,
|
||||||
|
uuid: Uuid,
|
||||||
|
) -> Option<EntityId> {
|
||||||
|
match self.uuid_to_entity.entry(uuid) {
|
||||||
|
Entry::Occupied(_) => None,
|
||||||
|
Entry::Vacant(ve) => {
|
||||||
|
let app = appearance.into();
|
||||||
|
|
||||||
|
let entity = self.comps.create_item();
|
||||||
|
|
||||||
|
ve.insert(entity);
|
||||||
|
|
||||||
|
if let (Some(aabb), Some(world)) = (app.aabb(), app.world()) {
|
||||||
|
self.partition_insert(entity, world, aabb);
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = entity.0.idx as usize;
|
||||||
|
if idx >= self.uuids.len() {
|
||||||
|
self.uuids.push(uuid);
|
||||||
|
self.clients.get_mut().push(MaybeClient(None));
|
||||||
|
self.appearances.get_mut().push(app.clone());
|
||||||
|
self.old_appearances.push(app);
|
||||||
|
} else {
|
||||||
|
self.uuids[idx] = uuid;
|
||||||
|
self.clients.get_mut()[idx].0 = None;
|
||||||
|
self.appearances.get_mut()[idx] = app.clone();
|
||||||
|
self.old_appearances[idx] = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&mut self, entity: EntityId) -> bool {
|
||||||
|
if self.comps.delete_item(entity) {
|
||||||
|
let idx = entity.0.idx as usize;
|
||||||
|
|
||||||
|
self.uuid_to_entity
|
||||||
|
.remove(&self.uuids[idx])
|
||||||
|
.expect("UUID should have been in UUID map");
|
||||||
|
|
||||||
|
self.clients.get_mut()[idx].0 = None;
|
||||||
|
|
||||||
|
let app = &self.appearances.get_mut()[idx];
|
||||||
|
if let (Some(aabb), Some(world)) = (app.aabb(), app.world()) {
|
||||||
|
self.partition_remove(entity, world, aabb);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of live entities.
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.comps.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_valid(&self, entity: EntityId) -> bool {
|
||||||
|
self.comps.is_valid(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<Z>(&self, z: Z, entity: EntityId) -> Option<Z::Item>
|
||||||
|
where
|
||||||
|
Z: ZippedComponents<Id = EntityId>,
|
||||||
|
{
|
||||||
|
self.comps.get(z, entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter<'a, Z>(&'a self, z: Z) -> impl FusedIterator<Item = (EntityId, Z::Item)> + 'a
|
||||||
|
where
|
||||||
|
Z: ZippedComponents<Id = EntityId> + 'a,
|
||||||
|
{
|
||||||
|
self.comps.iter(z)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn par_iter<'a, Z>(&'a self, z: Z) -> impl ParallelIterator<Item = (EntityId, Z::Item)> + 'a
|
||||||
|
where
|
||||||
|
Z: ZippedComponents<Id = EntityId> + 'a,
|
||||||
|
{
|
||||||
|
self.comps.par_iter(z)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ids(&self) -> impl FusedIterator<Item = EntityId> + Clone + '_ {
|
||||||
|
self.comps.ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn par_ids(&self) -> impl ParallelIterator<Item = EntityId> + Clone + '_ {
|
||||||
|
self.comps.par_ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uuids(&self) -> Uuids {
|
||||||
|
Uuids { uuids: &self.uuids }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clients(&self) -> Result<Clients, Error> {
|
||||||
|
Ok(Clients {
|
||||||
|
clients: self.clients.try_read().ok_or(Error::NoReadAccess)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clients_mut(&self) -> Result<ClientsMut, Error> {
|
||||||
|
Ok(ClientsMut {
|
||||||
|
clients: self.clients.try_write().ok_or(Error::NoWriteAccess)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn appearances(&self) -> Result<Appearances, Error> {
|
||||||
|
Ok(Appearances {
|
||||||
|
appearances: self.appearances.try_read().ok_or(Error::NoReadAccess)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn appearances_mut(&self) -> Result<AppearancesMut, Error> {
|
||||||
|
Ok(AppearancesMut {
|
||||||
|
appearances: self.appearances.try_write().ok_or(Error::NoWriteAccess)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn old_appearances(&self) -> OldAppearances {
|
||||||
|
OldAppearances {
|
||||||
|
old_appearances: &self.old_appearances,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_component<C: 'static + Send + Sync + Default>(&mut self) {
|
||||||
|
self.comps.register_component::<C>();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister_component<C: 'static + Send + Sync + Default>(&mut self) {
|
||||||
|
self.comps.unregister_component::<C>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_registered<C: 'static + Send + Sync + Default>(&self) -> bool {
|
||||||
|
self.comps.is_registered::<C>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn components<C: 'static + Send + Sync + Default>(
|
||||||
|
&self,
|
||||||
|
) -> Result<Components<C, EntityId>, Error> {
|
||||||
|
self.comps.components::<C>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn components_mut<C: 'static + Send + Sync + Default>(
|
||||||
|
&self,
|
||||||
|
) -> Result<ComponentsMut<C, EntityId>, Error> {
|
||||||
|
self.comps.components_mut::<C>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn partition_insert(&mut self, entity: EntityId, world: WorldId, aabb: Aabb<f64, 3>) {
|
||||||
|
let min_corner = ChunkPos::from_xz(aabb.min().xz());
|
||||||
|
let max_corner = ChunkPos::from_xz(aabb.max().xz());
|
||||||
|
|
||||||
|
for z in min_corner.z..=max_corner.z {
|
||||||
|
for x in min_corner.x..=max_corner.x {
|
||||||
|
self.partition_insert_at(entity, world, ChunkPos { x, z })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn partition_insert_at(&mut self, entity: EntityId, world: WorldId, pos: ChunkPos) {
|
||||||
|
match self.partition.entry((world, pos)) {
|
||||||
|
Entry::Occupied(mut oe) => {
|
||||||
|
debug_assert!(
|
||||||
|
!oe.get_mut().contains(&entity),
|
||||||
|
"spatial partition: entity already present"
|
||||||
|
);
|
||||||
|
oe.get_mut().push(entity);
|
||||||
|
}
|
||||||
|
Entry::Vacant(ve) => {
|
||||||
|
ve.insert(vec![entity]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn partition_remove(&mut self, entity: EntityId, world: WorldId, aabb: Aabb<f64, 3>) {
|
||||||
|
let min_corner = ChunkPos::from_xz(aabb.min().xz());
|
||||||
|
let max_corner = ChunkPos::from_xz(aabb.max().xz());
|
||||||
|
|
||||||
|
for z in min_corner.z..=max_corner.z {
|
||||||
|
for x in min_corner.x..=max_corner.x {
|
||||||
|
self.partition_remove_at(entity, world, ChunkPos::new(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn partition_remove_at(&mut self, entity: EntityId, world: WorldId, pos: ChunkPos) {
|
||||||
|
let errmsg = "spatial partition: entity removal failed";
|
||||||
|
match self.partition.entry((world, pos)) {
|
||||||
|
Entry::Occupied(mut oe) => {
|
||||||
|
let v = oe.get_mut();
|
||||||
|
let idx = v.iter().position(|e| *e == entity).expect(errmsg);
|
||||||
|
v.swap_remove(idx);
|
||||||
|
|
||||||
|
if v.is_empty() {
|
||||||
|
oe.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Entry::Vacant(_) => panic!("{errmsg}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn partition_modify(
|
||||||
|
&mut self,
|
||||||
|
entity: EntityId,
|
||||||
|
old_world: WorldId,
|
||||||
|
old_aabb: Aabb<f64, 3>,
|
||||||
|
new_world: WorldId,
|
||||||
|
new_aabb: Aabb<f64, 3>,
|
||||||
|
) {
|
||||||
|
if old_world != new_world {
|
||||||
|
self.partition_remove(entity, old_world, old_aabb);
|
||||||
|
self.partition_insert(entity, new_world, new_aabb);
|
||||||
|
} else {
|
||||||
|
let old_min_corner = ChunkPos::from_xz(old_aabb.min().xz());
|
||||||
|
let old_max_corner = ChunkPos::from_xz(old_aabb.max().xz());
|
||||||
|
let new_min_corner = ChunkPos::from_xz(new_aabb.min().xz());
|
||||||
|
let new_max_corner = ChunkPos::from_xz(new_aabb.max().xz());
|
||||||
|
|
||||||
|
for z in new_min_corner.z..=new_max_corner.z {
|
||||||
|
for x in new_min_corner.x..=new_max_corner.x {
|
||||||
|
if x < old_min_corner.x
|
||||||
|
|| x > old_max_corner.x
|
||||||
|
|| z < old_min_corner.z
|
||||||
|
|| z > old_max_corner.z
|
||||||
|
{
|
||||||
|
self.partition_insert_at(entity, old_world, ChunkPos::new(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for z in old_min_corner.z..=old_max_corner.z {
|
||||||
|
for x in old_min_corner.x..=old_max_corner.x {
|
||||||
|
if x < new_min_corner.x
|
||||||
|
|| x > new_max_corner.x
|
||||||
|
|| z < new_min_corner.z
|
||||||
|
|| z > new_max_corner.z
|
||||||
|
{
|
||||||
|
self.partition_remove_at(entity, old_world, ChunkPos::new(x, z))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all entities with bounding volumes intersecting
|
||||||
|
/// the given AABB in an arbitrary order.
|
||||||
|
pub fn intersecting_aabb(
|
||||||
|
&self,
|
||||||
|
world: WorldId,
|
||||||
|
aabb: Aabb<f64, 3>,
|
||||||
|
) -> impl FusedIterator<Item = EntityId> + '_ {
|
||||||
|
let min_corner = ChunkPos::from_xz(aabb.min().xz());
|
||||||
|
let max_corner = ChunkPos::from_xz(aabb.max().xz());
|
||||||
|
(min_corner.z..=max_corner.z).flat_map(move |z| {
|
||||||
|
(min_corner.x..=max_corner.x).flat_map(move |x| {
|
||||||
|
self.partition
|
||||||
|
.get(&(world, ChunkPos::new(x, z)))
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(move |v| {
|
||||||
|
v.iter().cloned().filter(move |&e| {
|
||||||
|
self.get(&self.old_appearances(), e)
|
||||||
|
.expect("spatial partition contains expired entity")
|
||||||
|
.aabb()
|
||||||
|
.expect("spatial partition contains entity without AABB")
|
||||||
|
.collides_with_aabb(&aabb)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_old_appearances(&mut self) {
|
||||||
|
for (old, new) in self
|
||||||
|
.old_appearances
|
||||||
|
.iter_mut()
|
||||||
|
.zip(self.appearances.get_mut().iter())
|
||||||
|
{
|
||||||
|
old.clone_from(new);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct EntityId(IdData);
|
||||||
|
|
||||||
|
impl IdRaw for EntityId {
|
||||||
|
fn from_data(data: IdData) -> Self {
|
||||||
|
Self(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_data(self) -> IdData {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntityId {
|
||||||
|
/// The vaule of the default `EntityId` which always refers to an expired
|
||||||
|
/// entity.
|
||||||
|
pub const NULL: Self = Self(IdData::NULL);
|
||||||
|
|
||||||
|
pub(crate) fn to_network_id(self) -> i32 {
|
||||||
|
self.0.idx as i32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Id for EntityId {}
|
||||||
|
|
||||||
|
/// A built-in component collection containing the UUID of the entities.
|
||||||
|
///
|
||||||
|
/// The default value for this component is a random unassigned UUID. UUIDs
|
||||||
|
/// cannot be modified after an entity is created.
|
||||||
|
///
|
||||||
|
/// TODO: describe the UUID for players.
|
||||||
|
pub struct Uuids<'a> {
|
||||||
|
uuids: &'a Vec<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b Uuids<'a> {
|
||||||
|
type RawItem = Uuid;
|
||||||
|
type RawIter = std::iter::Cloned<std::slice::Iter<'b, Uuid>>;
|
||||||
|
type RawParIter = rayon::iter::Cloned<rayon::slice::Iter<'b, Uuid>>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
self.uuids[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.uuids.iter().cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.uuids.par_iter().cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b Uuids<'a> {
|
||||||
|
type Id = EntityId;
|
||||||
|
type Item = Uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A built-in component collection containing the clients that entites are
|
||||||
|
/// backed by, if any.
|
||||||
|
///
|
||||||
|
/// When a client joins the server, a new entity is created which is backed by
|
||||||
|
/// the new client. However, when a client is disconnected, the entity which
|
||||||
|
/// they inhabited is _not_ automatically deleted.
|
||||||
|
///
|
||||||
|
/// Deleting the associated entity while the client is still connected will
|
||||||
|
/// immediately disconnect the client.
|
||||||
|
///
|
||||||
|
/// The default value of this component will not contain a client and all calls
|
||||||
|
/// to [`get`](Self::get) and [`get_mut`](Self::get_mut) will return `None`.
|
||||||
|
|
||||||
|
pub struct Clients<'a> {
|
||||||
|
// TODO: box the clients
|
||||||
|
clients: RwLockReadGuard<'a, Vec<MaybeClient>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b Clients<'a> {
|
||||||
|
type RawItem = &'b MaybeClient;
|
||||||
|
type RawIter = std::slice::Iter<'b, MaybeClient>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, MaybeClient>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.clients[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.clients.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.clients.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b Clients<'a> {
|
||||||
|
type Id = EntityId;
|
||||||
|
type Item = &'b MaybeClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ClientsMut<'a> {
|
||||||
|
clients: RwLockWriteGuard<'a, Vec<MaybeClient>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b ClientsMut<'a> {
|
||||||
|
type RawItem = &'b MaybeClient;
|
||||||
|
type RawIter = std::slice::Iter<'b, MaybeClient>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, MaybeClient>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.clients[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.clients.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.clients.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b ClientsMut<'a> {
|
||||||
|
type Id = EntityId;
|
||||||
|
type Item = &'b MaybeClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b mut ClientsMut<'a> {
|
||||||
|
type RawItem = &'b mut MaybeClient;
|
||||||
|
type RawIter = std::slice::IterMut<'b, MaybeClient>;
|
||||||
|
type RawParIter = rayon::slice::IterMut<'b, MaybeClient>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&mut self.clients[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.clients.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.clients.par_iter_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b mut ClientsMut<'a> {
|
||||||
|
type Id = EntityId;
|
||||||
|
type Item = &'b mut MaybeClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Appearances<'a> {
|
||||||
|
appearances: RwLockReadGuard<'a, Vec<Appearance>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b Appearances<'a> {
|
||||||
|
type RawItem = &'b Appearance;
|
||||||
|
type RawIter = std::slice::Iter<'b, Appearance>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, Appearance>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.appearances[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.appearances.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.appearances.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b Appearances<'a> {
|
||||||
|
type Id = EntityId;
|
||||||
|
type Item = &'b Appearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppearancesMut<'a> {
|
||||||
|
appearances: RwLockWriteGuard<'a, Vec<Appearance>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b AppearancesMut<'a> {
|
||||||
|
type RawItem = &'b Appearance;
|
||||||
|
type RawIter = std::slice::Iter<'b, Appearance>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, Appearance>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.appearances[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.appearances.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.appearances.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b AppearancesMut<'a> {
|
||||||
|
type Id = EntityId;
|
||||||
|
type Item = &'b Appearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b mut AppearancesMut<'a> {
|
||||||
|
type RawItem = &'b mut Appearance;
|
||||||
|
type RawIter = std::slice::IterMut<'b, Appearance>;
|
||||||
|
type RawParIter = rayon::slice::IterMut<'b, Appearance>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&mut self.appearances[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.appearances.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.appearances.par_iter_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b mut AppearancesMut<'a> {
|
||||||
|
type Id = EntityId;
|
||||||
|
type Item = &'b mut Appearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains a snapshot of an entity's [`Appearance`] as it existed at the end
|
||||||
|
/// of the previous tick.
|
||||||
|
pub struct OldAppearances<'a> {
|
||||||
|
old_appearances: &'a Vec<Appearance>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b OldAppearances<'a> {
|
||||||
|
type RawItem = &'b Appearance;
|
||||||
|
type RawIter = std::slice::Iter<'b, Appearance>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, Appearance>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.old_appearances[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.old_appearances.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.old_appearances.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b OldAppearances<'a> {
|
||||||
|
type Id = EntityId;
|
||||||
|
type Item = &'b Appearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use appearance::Player;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::glm;
|
||||||
|
|
||||||
|
// TODO: better test: spawn a bunch of random entities, spawn a random AABB,
|
||||||
|
// assert collides_with_aabb consistency.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn space_partition() {
|
||||||
|
let mut entities = EntityStore::new();
|
||||||
|
|
||||||
|
let ids = [(16.0, 16.0, 16.0), (8.0, 8.0, 8.0), (10.0, 50.0, 10.0)]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(x, y, z)| entities.create(Player::new(glm::vec3(x, y, z), WorldId::NULL)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let outside = *ids.last().unwrap();
|
||||||
|
|
||||||
|
assert!(entities
|
||||||
|
.intersecting_aabb(
|
||||||
|
WorldId::NULL,
|
||||||
|
Aabb::new(glm::vec3(8.0, 8.0, 8.0), glm::vec3(16.0, 16.0, 16.0)),
|
||||||
|
)
|
||||||
|
.all(|id| ids.contains(&id) && id != outside));
|
||||||
|
}
|
||||||
|
}
|
81
src/entity/appearance.rs
Normal file
81
src/entity/appearance.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use glm::DVec3;
|
||||||
|
|
||||||
|
use crate::{glm, Aabb, WorldId};
|
||||||
|
|
||||||
|
// TODO: override default clone_from impl for Appearance.
|
||||||
|
/// Encodes the type of an entity (pig, player, item frame, etc.) along with any
|
||||||
|
/// state that would influence its appearance to clients.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Appearance {
|
||||||
|
/// The default appearance.
|
||||||
|
///
|
||||||
|
/// Entities with an appearance of `None` will not be visible to clients.
|
||||||
|
None,
|
||||||
|
Player(Player),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Appearance {
|
||||||
|
pub fn position(&self) -> Option<DVec3> {
|
||||||
|
match self {
|
||||||
|
Appearance::None => None,
|
||||||
|
Appearance::Player(p) => Some(p.position),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn position_mut(&mut self) -> Option<&mut DVec3> {
|
||||||
|
match self {
|
||||||
|
Appearance::None => None,
|
||||||
|
Appearance::Player(p) => Some(&mut p.position),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn world(&self) -> Option<WorldId> {
|
||||||
|
match self {
|
||||||
|
Appearance::None => None,
|
||||||
|
Appearance::Player(p) => Some(p.world),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn world_mut(&mut self) -> Option<&mut WorldId> {
|
||||||
|
match self {
|
||||||
|
Appearance::None => None,
|
||||||
|
Appearance::Player(p) => Some(&mut p.world),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn aabb(&self) -> Option<Aabb<f64, 3>> {
|
||||||
|
match self {
|
||||||
|
Appearance::None => None,
|
||||||
|
Appearance::Player(p) => Some(p.aabb()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Appearance {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Player {
|
||||||
|
pub position: DVec3,
|
||||||
|
pub world: WorldId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
pub fn new(position: DVec3, world: WorldId) -> Self {
|
||||||
|
Self { position, world }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn aabb(&self) -> Aabb<f64, 3> {
|
||||||
|
// TODO: player hitbox dimensions change depending on pose
|
||||||
|
Aabb::from_center_and_dimensions(self.position, glm::vec3(0.6, 1.8, 0.6))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Player> for Appearance {
|
||||||
|
fn from(p: Player) -> Self {
|
||||||
|
Self::Player(p)
|
||||||
|
}
|
||||||
|
}
|
288
src/identifier.rs
Normal file
288
src/identifier.rs
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use ascii::{AsAsciiStr, AsciiChar, AsciiStr, IntoAsciiString};
|
||||||
|
use serde::de::Visitor;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::protocol::{encode_string_bounded, BoundedString, Decode, Encode};
|
||||||
|
|
||||||
|
/// An identifier is a string split into a "namespace" part and a "name" part.
|
||||||
|
/// For instance `minecraft:apple` and `apple` are both valid identifiers.
|
||||||
|
///
|
||||||
|
/// If the namespace part is left off (the part before and including the colon)
|
||||||
|
/// the namespace is considered to be "minecraft".
|
||||||
|
///
|
||||||
|
/// The entire identifier must match the regex `([a-z0-9_-]+:)?[a-z0-9_\/.-]+`.
|
||||||
|
#[derive(Clone, Eq)]
|
||||||
|
pub struct Identifier {
|
||||||
|
ident: Cow<'static, AsciiStr>,
|
||||||
|
/// The index of the ':' character in the string.
|
||||||
|
/// If there is no namespace then it is `usize::MAX`.
|
||||||
|
///
|
||||||
|
/// Since the string only contains ASCII characters, we can slice it
|
||||||
|
/// in O(1) time.
|
||||||
|
colon_idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Error, PartialEq, Eq, Debug)]
|
||||||
|
#[error("invalid identifier \"{src}\"")]
|
||||||
|
pub struct ParseError {
|
||||||
|
src: Cow<'static, str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Identifier {
|
||||||
|
/// Parses a new identifier from a string.
|
||||||
|
///
|
||||||
|
/// The string must match the regex `([a-z0-9_-]+:)?[a-z0-9_\/.-]+`.
|
||||||
|
/// If not, an error is returned.
|
||||||
|
pub fn new(str: impl Into<Cow<'static, str>>) -> Result<Identifier, ParseError> {
|
||||||
|
#![allow(bindings_with_variant_name)]
|
||||||
|
|
||||||
|
let cow = match str.into() {
|
||||||
|
Cow::Borrowed(s) => {
|
||||||
|
Cow::Borrowed(s.as_ascii_str().map_err(|_| ParseError { src: s.into() })?)
|
||||||
|
}
|
||||||
|
Cow::Owned(s) => Cow::Owned(s.into_ascii_string().map_err(|e| ParseError {
|
||||||
|
src: e.into_source().into(),
|
||||||
|
})?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let s = cow.as_ref();
|
||||||
|
|
||||||
|
let check_namespace = |s: &AsciiStr| {
|
||||||
|
!s.is_empty()
|
||||||
|
&& s.chars()
|
||||||
|
.all(|c| matches!(c.as_char(), 'a'..='z' | '0'..='9' | '_' | '-'))
|
||||||
|
};
|
||||||
|
let check_name = |s: &AsciiStr| {
|
||||||
|
!s.is_empty()
|
||||||
|
&& s.chars()
|
||||||
|
.all(|c| matches!(c.as_char(), 'a'..='z' | '0'..='9' | '_' | '/' | '.' | '-'))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(colon_idx) = s.chars().position(|c| c == AsciiChar::Colon) {
|
||||||
|
if check_namespace(&s[..colon_idx]) && check_name(&s[colon_idx + 1..]) {
|
||||||
|
Ok(Self {
|
||||||
|
ident: cow,
|
||||||
|
colon_idx,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(ParseError {
|
||||||
|
src: ascii_cow_to_str_cow(cow),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if check_name(s) {
|
||||||
|
Ok(Self {
|
||||||
|
ident: cow,
|
||||||
|
colon_idx: usize::MAX,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(ParseError {
|
||||||
|
src: ascii_cow_to_str_cow(cow),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the namespace part of this namespaced identifier.
|
||||||
|
/// If this identifier was constructed from a string without a namespace,
|
||||||
|
/// then `None` is returned.
|
||||||
|
pub fn namespace(&self) -> Option<&str> {
|
||||||
|
if self.colon_idx == usize::MAX {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.ident[..self.colon_idx].as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the name part of this namespaced identifier.
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
if self.colon_idx == usize::MAX {
|
||||||
|
self.ident.as_str()
|
||||||
|
} else {
|
||||||
|
self.ident[self.colon_idx + 1..].as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the identifier as a `str`.
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.ident.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ascii_cow_to_str_cow(cow: Cow<AsciiStr>) -> Cow<str> {
|
||||||
|
match cow {
|
||||||
|
Cow::Borrowed(s) => Cow::Borrowed(s.as_str()),
|
||||||
|
Cow::Owned(s) => Cow::Owned(s.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseError {
|
||||||
|
pub fn into_source(self) -> Cow<'static, str> {
|
||||||
|
self.src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Identifier {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_tuple("Identifier").field(&self.as_str()).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Identifier {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Identifier::new(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Identifier> for String {
|
||||||
|
fn from(id: Identifier) -> Self {
|
||||||
|
id.ident.into_owned().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Identifier> for Cow<'static, str> {
|
||||||
|
fn from(id: Identifier) -> Self {
|
||||||
|
ascii_cow_to_str_cow(id.ident)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for Identifier {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for Identifier {
|
||||||
|
type Error = ParseError;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
Identifier::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&'static str> for Identifier {
|
||||||
|
type Error = ParseError;
|
||||||
|
|
||||||
|
fn try_from(value: &'static str) -> Result<Self, Self::Error> {
|
||||||
|
Identifier::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Identifier {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Equality for identifiers respects the fact that "minecraft:apple" and
|
||||||
|
/// "apple" have the same meaning.
|
||||||
|
impl PartialEq for Identifier {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.namespace().unwrap_or("minecraft") == other.namespace().unwrap_or("minecraft")
|
||||||
|
&& self.name() == other.name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::hash::Hash for Identifier {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.namespace().unwrap_or("minecraft").hash(state);
|
||||||
|
self.name().hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for Identifier {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
encode_string_bounded(self.as_str(), 0, 32767, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for Identifier {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let string = BoundedString::<0, 32767>::decode(r)?.0;
|
||||||
|
Ok(Identifier::new(string)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Identifier {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
self.as_str().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Identifier {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
deserializer.deserialize_str(IdentifierVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An implementation of `serde::de::Visitor` for Minecraft identifiers.
|
||||||
|
struct IdentifierVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for IdentifierVisitor {
|
||||||
|
type Value = Identifier;
|
||||||
|
|
||||||
|
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "a valid Minecraft identifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
|
||||||
|
Identifier::from_str(s).map_err(E::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_string<E: serde::de::Error>(self, s: String) -> Result<Self::Value, E> {
|
||||||
|
Identifier::new(s).map_err(E::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience macro for constructing an identifier from a format string.
|
||||||
|
///
|
||||||
|
/// The macro will panic if the formatted string is not a valid
|
||||||
|
/// identifier.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! ident {
|
||||||
|
($($arg:tt)*) => {{
|
||||||
|
let errmsg = "invalid identifier in `ident` macro";
|
||||||
|
#[allow(clippy::redundant_closure_call)]
|
||||||
|
(|args: ::std::fmt::Arguments| match args.as_str() {
|
||||||
|
Some(s) => $crate::Identifier::new(s).expect(errmsg),
|
||||||
|
None => $crate::Identifier::new(args.to_string()).expect(errmsg),
|
||||||
|
})(format_args!($($arg)*))
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn parse_valid() {
|
||||||
|
ident!("minecraft:whatever");
|
||||||
|
ident!("_what-ever55_:.whatever/whatever123456789_");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn parse_invalid_0() {
|
||||||
|
ident!("");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn parse_invalid_1() {
|
||||||
|
ident!(":");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn parse_invalid_2() {
|
||||||
|
ident!("foo:bar:baz");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn equality() {
|
||||||
|
assert_eq!(ident!("minecraft:my.identifier"), ident!("my.identifier"));
|
||||||
|
}
|
||||||
|
}
|
49
src/lib.rs
49
src/lib.rs
|
@ -0,0 +1,49 @@
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
mod aabb;
|
||||||
|
mod block_pos;
|
||||||
|
mod byte_angle;
|
||||||
|
mod chunk;
|
||||||
|
mod chunk_store;
|
||||||
|
mod client;
|
||||||
|
mod codec;
|
||||||
|
pub mod component;
|
||||||
|
pub mod config;
|
||||||
|
pub mod entity;
|
||||||
|
pub mod identifier;
|
||||||
|
mod packets;
|
||||||
|
mod protocol;
|
||||||
|
mod server;
|
||||||
|
pub mod text;
|
||||||
|
pub mod util;
|
||||||
|
mod var_int;
|
||||||
|
mod var_long;
|
||||||
|
mod world;
|
||||||
|
|
||||||
|
pub use aabb::Aabb;
|
||||||
|
pub use chunk::{Chunk, ChunkPos};
|
||||||
|
pub use client::Client;
|
||||||
|
pub use config::{BiomeId, DimensionId, ServerConfig};
|
||||||
|
pub use entity::{EntityId, EntityStore};
|
||||||
|
pub use identifier::Identifier;
|
||||||
|
pub use text::{Text, TextFormat};
|
||||||
|
pub use uuid::Uuid;
|
||||||
|
pub use world::{World, WorldId};
|
||||||
|
pub use {nalgebra_glm as glm, nbt};
|
||||||
|
|
||||||
|
pub use crate::server::{NewClientData, Server, SharedServer, ShutdownResult};
|
||||||
|
|
||||||
|
/// The Minecraft protocol version that this library targets.
|
||||||
|
pub const PROTOCOL_VERSION: i32 = 758;
|
||||||
|
/// The name of the Minecraft version that this library targets.
|
||||||
|
pub const VERSION_NAME: &str = "1.18.2";
|
||||||
|
|
||||||
|
/// The namespace for this library used internally for namespaced identifiers.
|
||||||
|
const LIBRARY_NAMESPACE: &str = "valence";
|
||||||
|
|
||||||
|
/// A discrete unit of time where 1 tick is the duration of a
|
||||||
|
/// single game update.
|
||||||
|
///
|
||||||
|
/// The duration of a game update depends on the current configuration, which
|
||||||
|
/// may or may not be the same as Minecraft's standard 20 ticks/second.
|
||||||
|
pub type Ticks = i64;
|
1815
src/packets.rs
Normal file
1815
src/packets.rs
Normal file
File diff suppressed because it is too large
Load diff
548
src/protocol.rs
Normal file
548
src/protocol.rs
Normal file
|
@ -0,0 +1,548 @@
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, ensure};
|
||||||
|
use bitvec::prelude::*;
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::var_int::VarInt;
|
||||||
|
|
||||||
|
/// Trait for types that can be written to the Minecraft protocol.
|
||||||
|
pub trait Encode {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for types that can be constructed from the Minecraft protocol.
|
||||||
|
pub trait Decode: Sized {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The maximum number of bytes in a single packet.
|
||||||
|
pub const MAX_PACKET_SIZE: i32 = 2097151;
|
||||||
|
|
||||||
|
impl Encode for () {
|
||||||
|
fn encode(&self, _w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for () {
|
||||||
|
fn decode(_r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Encode> Encode for &T {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
(*self).encode(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for bool {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_u8(*self as u8)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for bool {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let n = r.read_u8()?;
|
||||||
|
ensure!(n < 2, "boolean is not 0 or 1");
|
||||||
|
Ok(n == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for u8 {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_u8(*self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for u8 {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(r.read_u8()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for i8 {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_i8(*self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for i8 {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(r.read_i8()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for u16 {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_u16::<BigEndian>(*self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for u16 {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(r.read_u16::<BigEndian>()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for i16 {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_i16::<BigEndian>(*self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for i16 {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(r.read_i16::<BigEndian>()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for u32 {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_u32::<BigEndian>(*self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for u32 {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(r.read_u32::<BigEndian>()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for i32 {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_i32::<BigEndian>(*self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for i32 {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(r.read_i32::<BigEndian>()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for u64 {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_u64::<BigEndian>(*self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for u64 {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(r.read_u64::<BigEndian>()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for i64 {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_i64::<BigEndian>(*self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for i64 {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(r.read_i64::<BigEndian>()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for f32 {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
ensure!(
|
||||||
|
self.is_finite(),
|
||||||
|
"attempt to encode non-finite f32 ({})",
|
||||||
|
self
|
||||||
|
);
|
||||||
|
w.write_f32::<BigEndian>(*self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Decode for f32 {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let f = r.read_f32::<BigEndian>()?;
|
||||||
|
ensure!(f.is_finite(), "attempt to decode non-finite f32 ({f})");
|
||||||
|
Ok(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for f64 {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
ensure!(
|
||||||
|
self.is_finite(),
|
||||||
|
"attempt to encode non-finite f64 ({})",
|
||||||
|
self
|
||||||
|
);
|
||||||
|
w.write_f64::<BigEndian>(*self)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for f64 {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let f = r.read_f64::<BigEndian>()?;
|
||||||
|
ensure!(f.is_finite(), "attempt to decode non-finite f64 ({f})");
|
||||||
|
Ok(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Encode> Encode for Option<T> {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
match self {
|
||||||
|
Some(t) => true.encode(w).and_then(|_| t.encode(w)),
|
||||||
|
None => false.encode(w),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Decode> Decode for Option<T> {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
if bool::decode(r)? {
|
||||||
|
Ok(Some(T::decode(r)?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
|
pub struct BoundedInt<T, const MIN: i64, const MAX: i64>(pub T);
|
||||||
|
|
||||||
|
impl<T, const MIN: i64, const MAX: i64> From<T> for BoundedInt<T, MIN, MAX> {
|
||||||
|
fn from(t: T) -> Self {
|
||||||
|
Self(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const MIN: i64, const MAX: i64> Encode for BoundedInt<T, MIN, MAX>
|
||||||
|
where
|
||||||
|
T: Encode + Copy + Into<i64>,
|
||||||
|
{
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
let val = self.0.into();
|
||||||
|
ensure!(
|
||||||
|
(MIN..=MAX).contains(&val),
|
||||||
|
"Integer is not in bounds while encoding (got {val}, expected {MIN}..={MAX})"
|
||||||
|
);
|
||||||
|
|
||||||
|
self.0.encode(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const MIN: i64, const MAX: i64> Decode for BoundedInt<T, MIN, MAX>
|
||||||
|
where
|
||||||
|
T: Decode + Copy + Into<i64>,
|
||||||
|
{
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let res = T::decode(r)?;
|
||||||
|
let val = res.into();
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
(MIN..=MAX).contains(&val),
|
||||||
|
"Integer is not in bounds while decoding (got {val}, expected {MIN}..={MAX})"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self(res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: bounded float?
|
||||||
|
|
||||||
|
impl Encode for String {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
encode_string_bounded(self, 0, 32767, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for String {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
decode_string_bounded(0, 32767, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A string with a minimum and maximum character length known at compile time.
|
||||||
|
/// If the string is not in bounds, an error is generated while
|
||||||
|
/// encoding/decoding.
|
||||||
|
///
|
||||||
|
/// Note that the length is a count of the characters in the string, not bytes.
|
||||||
|
///
|
||||||
|
/// When encoded and decoded, the string is VarInt prefixed.
|
||||||
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Debug)]
|
||||||
|
pub struct BoundedString<const MIN: usize, const MAX: usize>(pub String);
|
||||||
|
|
||||||
|
impl<const MIN: usize, const MAX: usize> Encode for BoundedString<MIN, MAX> {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
encode_string_bounded(&self.0, MIN, MAX, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const MIN: usize, const MAX: usize> Decode for BoundedString<MIN, MAX> {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
decode_string_bounded(MIN, MAX, r).map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const MIN: usize, const MAX: usize> From<String> for BoundedString<MIN, MAX> {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Encode> Encode for Vec<T> {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
encode_array_bounded(self, 0, usize::MAX, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Decode> Decode for Vec<T> {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
decode_array_bounded(0, usize::MAX, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Encode> Encode for Box<[T]> {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
encode_array_bounded(self, 0, usize::MAX, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Decode> Decode for Box<[T]> {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
decode_array_bounded(0, usize::MAX, r).map(|v| v.into_boxed_slice())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Encode, const N: usize> Encode for [T; N] {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
encode_array_bounded(self, N, N, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Decode, const N: usize> Decode for [T; N] {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let vec = decode_array_bounded(N, N, r)?;
|
||||||
|
match vec.try_into() {
|
||||||
|
Ok(arr) => Ok(arr),
|
||||||
|
Err(_) => unreachable!("array size does not match"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An array with a minimum and maximum character length known at compile time.
|
||||||
|
/// If the array is not in bounds, an error is generated while
|
||||||
|
/// encoding/decoding.
|
||||||
|
///
|
||||||
|
/// When encoding/decoding, the array is VarInt prefixed.
|
||||||
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Debug)]
|
||||||
|
pub struct BoundedArray<T, const MIN: usize = 0, const MAX: usize = { usize::MAX }>(pub Vec<T>);
|
||||||
|
|
||||||
|
impl<T: Encode, const MIN: usize, const MAX: usize> Encode for BoundedArray<T, MIN, MAX> {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
encode_array_bounded(&self.0, MIN, MAX, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Decode, const MIN: usize, const MAX: usize> Decode for BoundedArray<T, MIN, MAX> {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
decode_array_bounded(MIN, MAX, r).map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const MIN: usize, const MAX: usize> From<Vec<T>> for BoundedArray<T, MIN, MAX> {
|
||||||
|
fn from(v: Vec<T>) -> Self {
|
||||||
|
Self(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for Uuid {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_u128::<BigEndian>(self.as_u128())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for Uuid {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(Uuid::from_u128(r.read_u128::<BigEndian>()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for nbt::Blob {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
Ok(nbt::to_writer(w, self, None)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for nbt::Blob {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
Ok(nbt::from_reader(r)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper type acting as a bridge between Serde and [Encode]/[Decode] through
|
||||||
|
/// the NBT format.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Debug)]
|
||||||
|
pub struct Nbt<T>(pub T);
|
||||||
|
|
||||||
|
impl<T: Serialize> Encode for Nbt<T> {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
let mut enc = nbt::ser::Encoder::new(w, None);
|
||||||
|
self.0.serialize(&mut enc)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Deserialize<'a>> Decode for Nbt<T> {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let mut dec = nbt::de::Decoder::new(r);
|
||||||
|
Ok(Nbt(Deserialize::deserialize(&mut dec)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for BitVec<u64> {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
encode_array_bounded(self.as_raw_slice(), 0, usize::MAX, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for BitVec<u64> {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
BitVec::try_from_vec(Vec::decode(r)?)
|
||||||
|
.map_err(|_| anyhow!("Array is too long for bit vector"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for BitBox<u64> {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
encode_array_bounded(self.as_raw_slice(), 0, usize::MAX, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for BitBox<u64> {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
BitVec::decode(r).map(|v| v.into_boxed_bitslice())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When decoding, reads the rest of the data in a packet and stuffs it into a
|
||||||
|
/// `Vec<u8>`. When encoding, the data is inserted into the packet with no
|
||||||
|
/// length prefix.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ReadToEnd(pub Vec<u8>);
|
||||||
|
|
||||||
|
impl Decode for ReadToEnd {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.read_to_end(&mut buf)?;
|
||||||
|
Ok(ReadToEnd(buf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for ReadToEnd {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
w.write_all(&self.0).map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_array_bounded<T: Encode>(
|
||||||
|
s: &[T],
|
||||||
|
min: usize,
|
||||||
|
max: usize,
|
||||||
|
w: &mut impl Write,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
assert!(min <= max);
|
||||||
|
|
||||||
|
let len = s.len();
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
(min..=max).contains(&len),
|
||||||
|
"Length of array is out of bounds while encoding (got {len}, expected {min}..={max})"
|
||||||
|
);
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
len <= i32::MAX as usize,
|
||||||
|
"Length of array ({len}) exceeds i32::MAX"
|
||||||
|
);
|
||||||
|
|
||||||
|
VarInt(len as i32).encode(w)?;
|
||||||
|
for t in s {
|
||||||
|
t.encode(w)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn encode_string_bounded(
|
||||||
|
s: &str,
|
||||||
|
min: usize,
|
||||||
|
max: usize,
|
||||||
|
w: &mut impl Write,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
assert!(min <= max, "Bad min and max");
|
||||||
|
|
||||||
|
let char_count = s.chars().count();
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
(min..=max).contains(&char_count),
|
||||||
|
"Char count of string is out of bounds while encoding (got {char_count}, expected \
|
||||||
|
{min}..={max})"
|
||||||
|
);
|
||||||
|
|
||||||
|
encode_array_bounded(s.as_bytes(), 0, usize::MAX, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn decode_string_bounded(
|
||||||
|
min: usize,
|
||||||
|
max: usize,
|
||||||
|
r: &mut impl Read,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
assert!(min <= max);
|
||||||
|
|
||||||
|
let bytes = decode_array_bounded(min, max.saturating_mul(4), r)?;
|
||||||
|
let string = String::from_utf8(bytes)?;
|
||||||
|
|
||||||
|
let char_count = string.chars().count();
|
||||||
|
ensure!(
|
||||||
|
(min..=max).contains(&char_count),
|
||||||
|
"Char count of string is out of bounds while decoding (got {char_count}, expected \
|
||||||
|
{min}..={max}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn decode_array_bounded<T: Decode>(
|
||||||
|
min: usize,
|
||||||
|
max: usize,
|
||||||
|
r: &mut impl Read,
|
||||||
|
) -> anyhow::Result<Vec<T>> {
|
||||||
|
assert!(min <= max);
|
||||||
|
|
||||||
|
let len = VarInt::decode(r)?.0;
|
||||||
|
ensure!(
|
||||||
|
len >= 0 && (min..=max).contains(&(len as usize)),
|
||||||
|
"Length of array is out of bounds while decoding (got {len}, needed {min}..={max})",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't allocate more than what would roughly fit in a single packet in case we
|
||||||
|
// get a malicious array length.
|
||||||
|
let cap = (MAX_PACKET_SIZE as usize / mem::size_of::<T>().max(1)).min(len as usize);
|
||||||
|
|
||||||
|
let mut res = Vec::with_capacity(cap);
|
||||||
|
for _ in 0..len {
|
||||||
|
res.push(T::decode(r)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
785
src/server.rs
Normal file
785
src/server.rs
Normal file
|
@ -0,0 +1,785 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use anyhow::{bail, ensure, Context};
|
||||||
|
use flume::{Receiver, Sender};
|
||||||
|
use num::BigInt;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use rayon::iter::ParallelIterator;
|
||||||
|
use reqwest::Client as HttpClient;
|
||||||
|
use rsa::{PaddingScheme, PublicKeyParts, RsaPrivateKey};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use sha1::digest::Update;
|
||||||
|
use sha1::Sha1;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
||||||
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
use tokio::runtime::{Handle, Runtime};
|
||||||
|
use tokio::sync::{oneshot, Semaphore};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::chunk_store::ChunkStore;
|
||||||
|
use crate::client::MaybeClient;
|
||||||
|
use crate::codec::{Decoder, Encoder};
|
||||||
|
use crate::config::{Biome, BiomeId, Dimension, DimensionId, Handler, Login, ServerListPing};
|
||||||
|
use crate::entity::Appearance;
|
||||||
|
use crate::packets::handshake::{Handshake, HandshakeNextState};
|
||||||
|
use crate::packets::login::{
|
||||||
|
self, EncryptionRequest, EncryptionResponse, LoginStart, LoginSuccess, SetCompression,
|
||||||
|
};
|
||||||
|
use crate::packets::play::{ClientPlayPacket, ServerPlayPacket};
|
||||||
|
use crate::packets::status::{Ping, Pong, Request, Response};
|
||||||
|
use crate::protocol::{BoundedArray, BoundedString};
|
||||||
|
use crate::util::valid_username;
|
||||||
|
use crate::var_int::VarInt;
|
||||||
|
use crate::world::WorldStore;
|
||||||
|
use crate::{Client, EntityStore, ServerConfig, Ticks, PROTOCOL_VERSION, VERSION_NAME};
|
||||||
|
|
||||||
|
/// Holds the state of a running Minecraft server which is accessible inside the
|
||||||
|
/// update loop. To start a server, see [`ServerConfig`].
|
||||||
|
///
|
||||||
|
/// Fields of this struct are made public to enable disjoint borrows. For
|
||||||
|
/// instance, it is possible to create and delete entities while
|
||||||
|
/// having read-only access to world data.
|
||||||
|
///
|
||||||
|
/// Note the `Deref` and `DerefMut` impls on `Server` are (ab)used to
|
||||||
|
/// allow convenient access to the `other` field.
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct Server {
|
||||||
|
pub entities: EntityStore,
|
||||||
|
pub worlds: WorldStore,
|
||||||
|
pub chunks: ChunkStore,
|
||||||
|
pub other: Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Other {
|
||||||
|
/// The shared portion of the server.
|
||||||
|
shared: SharedServer,
|
||||||
|
new_players_rx: Receiver<NewClientMessage>,
|
||||||
|
/// Incremented on every game tick.
|
||||||
|
tick_counter: Ticks,
|
||||||
|
/// The instant the current game tick began.
|
||||||
|
tick_start: Instant,
|
||||||
|
/// The time the last keep alive packet was sent to all players.
|
||||||
|
pub(crate) last_keepalive: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A server handle providing the subset of functionality which can be performed
|
||||||
|
/// outside the update loop. `SharedServer`s are interally refcounted and can be
|
||||||
|
/// freely cloned and shared between threads.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SharedServer(Arc<SharedServerInner>);
|
||||||
|
|
||||||
|
struct SharedServerInner {
|
||||||
|
handler: Box<dyn Handler>,
|
||||||
|
address: SocketAddr,
|
||||||
|
update_duration: Duration,
|
||||||
|
online_mode: bool,
|
||||||
|
max_clients: usize,
|
||||||
|
clientbound_packet_capacity: usize,
|
||||||
|
serverbound_packet_capacity: usize,
|
||||||
|
tokio_handle: Handle,
|
||||||
|
dimensions: Vec<Dimension>,
|
||||||
|
biomes: Vec<Biome>,
|
||||||
|
|
||||||
|
/// The instant the server was started.
|
||||||
|
start_instant: Instant,
|
||||||
|
/// A semaphore used to limit the number of simultaneous connections to the
|
||||||
|
/// server. Closing this semaphore stops new connections.
|
||||||
|
connection_sema: Arc<Semaphore>,
|
||||||
|
/// The result that will be returned when the server is shut down.
|
||||||
|
shutdown_result: Mutex<Option<ShutdownResult>>,
|
||||||
|
/// 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: HttpClient,
|
||||||
|
new_clients_tx: Sender<NewClientMessage>,
|
||||||
|
client_count: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains information about a new player.
|
||||||
|
pub struct NewClientData {
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
pub remote_addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NewClientMessage {
|
||||||
|
ncd: NewClientData,
|
||||||
|
reply: oneshot::Sender<anyhow::Result<ClientPacketChannels>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result type returned from [`ServerConfig::start`] after the server is shut
|
||||||
|
/// down.
|
||||||
|
pub type ShutdownResult = Result<(), ShutdownError>;
|
||||||
|
pub type ShutdownError = Box<dyn Error + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
pub(crate) type ClientPacketChannels = (Sender<ServerPlayPacket>, Receiver<ClientPlayPacket>);
|
||||||
|
pub(crate) type ServerPacketChannels = (Sender<ClientPlayPacket>, Receiver<ServerPlayPacket>);
|
||||||
|
|
||||||
|
impl Other {
|
||||||
|
/// Returns a reference to a [`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) -> Ticks {
|
||||||
|
self.tick_counter
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the instant the current tick began.
|
||||||
|
pub fn tick_start(&self) -> Instant {
|
||||||
|
self.tick_start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedServer {
|
||||||
|
pub fn handler(&self) -> &(impl Handler + ?Sized) {
|
||||||
|
self.0.handler.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn address(&self) -> SocketAddr {
|
||||||
|
self.0.address
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_duration(&self) -> Duration {
|
||||||
|
self.0.update_duration
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn online_mode(&self) -> bool {
|
||||||
|
self.0.online_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_clients(&self) -> usize {
|
||||||
|
self.0.max_clients
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clientbound_packet_capacity(&self) -> usize {
|
||||||
|
self.0.clientbound_packet_capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serverbound_packet_capacity(&self) -> usize {
|
||||||
|
self.0.serverbound_packet_capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tokio_handle(&self) -> &Handle {
|
||||||
|
&self.0.tokio_handle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtains a [`Dimension`] by using its corresponding [`DimensionId`].
|
||||||
|
///
|
||||||
|
/// It is safe but unspecified behavior to call this function using a
|
||||||
|
/// [`DimensionId`] not originating from the configuration used to construct
|
||||||
|
/// the server.
|
||||||
|
pub fn dimension(&self, id: DimensionId) -> &Dimension {
|
||||||
|
self.0
|
||||||
|
.dimensions
|
||||||
|
.get(id.0 as usize)
|
||||||
|
.expect("invalid dimension ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all added dimensions and their associated
|
||||||
|
/// [`DimensionId`].
|
||||||
|
pub fn dimensions(&self) -> impl FusedIterator<Item = (&Dimension, DimensionId)> + Clone {
|
||||||
|
self.0
|
||||||
|
.dimensions
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, d)| (d, DimensionId(i as u16)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtains a [`Biome`] by using its corresponding [`BiomeId`].
|
||||||
|
///
|
||||||
|
/// It is safe but unspecified behavior to call this function using a
|
||||||
|
/// [`BiomeId`] not originating from the configuration used to construct the
|
||||||
|
/// server.
|
||||||
|
pub fn biome(&self, id: BiomeId) -> &Biome {
|
||||||
|
self.0.biomes.get(id.0 as usize).expect("invalid biome ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all added biomes and their associated
|
||||||
|
/// [`BiomeId`].
|
||||||
|
pub fn biomes(&self) -> impl FusedIterator<Item = (&Biome, BiomeId)> + Clone {
|
||||||
|
self.0
|
||||||
|
.biomes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, b)| (b, BiomeId(i as u16)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the instant the server was started.
|
||||||
|
pub fn start_instant(&self) -> Instant {
|
||||||
|
self.0.start_instant
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Immediately stops new connections to the server and initiates server
|
||||||
|
/// shutdown. The given result is returned through [`ServerConfig::start`].
|
||||||
|
///
|
||||||
|
/// You may want to disconnect all players with a message prior to calling
|
||||||
|
/// this function.
|
||||||
|
pub fn shutdown<R, E>(&self, res: R)
|
||||||
|
where
|
||||||
|
R: Into<Result<(), E>>,
|
||||||
|
E: Into<Box<dyn Error + Send + Sync + 'static>>,
|
||||||
|
{
|
||||||
|
self.0.connection_sema.close();
|
||||||
|
*self.0.shutdown_result.lock() = Some(res.into().map_err(|e| e.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of clients past the login stage that are currently
|
||||||
|
/// connected to the server.
|
||||||
|
pub fn client_count(&self) -> usize {
|
||||||
|
self.0.client_count.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment the client count iff it is below the maximum number of
|
||||||
|
/// clients. Returns true if the client count was incremented, false
|
||||||
|
/// otherwise.
|
||||||
|
fn try_inc_player_count(&self) -> bool {
|
||||||
|
self.0
|
||||||
|
.client_count
|
||||||
|
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
|
||||||
|
if count < self.0.max_clients {
|
||||||
|
count.checked_add(1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dec_client_count(&self) {
|
||||||
|
let prev = self.0.client_count.fetch_sub(1, Ordering::SeqCst);
|
||||||
|
assert!(prev != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Server {
|
||||||
|
type Target = Other;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for Server {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Other {
|
||||||
|
type Target = SharedServer;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.shared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn start_server(config: ServerConfig) -> ShutdownResult {
|
||||||
|
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 (new_players_tx, new_players_rx) = flume::bounded(1);
|
||||||
|
|
||||||
|
let rt = if config.tokio_handle.is_none() {
|
||||||
|
Some(Runtime::new()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = match &rt {
|
||||||
|
Some(rt) => rt.handle().clone(),
|
||||||
|
None => config.tokio_handle.unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _guard = handle.enter();
|
||||||
|
|
||||||
|
let connection_sema = Arc::new(Semaphore::new(config.max_clients.saturating_add(64)));
|
||||||
|
|
||||||
|
struct DummyHandler;
|
||||||
|
impl Handler for DummyHandler {}
|
||||||
|
|
||||||
|
let shared = SharedServer(Arc::new(SharedServerInner {
|
||||||
|
handler: config.handler.unwrap_or_else(|| Box::new(DummyHandler)),
|
||||||
|
address: config.address,
|
||||||
|
update_duration: config.update_duration,
|
||||||
|
online_mode: config.online_mode,
|
||||||
|
max_clients: config.max_clients,
|
||||||
|
clientbound_packet_capacity: config.clientbound_packet_capacity,
|
||||||
|
serverbound_packet_capacity: config.serverbound_packet_capacity,
|
||||||
|
tokio_handle: handle.clone(),
|
||||||
|
dimensions: config.dimensions,
|
||||||
|
biomes: config.biomes,
|
||||||
|
start_instant: Instant::now(),
|
||||||
|
connection_sema,
|
||||||
|
shutdown_result: Mutex::new(None),
|
||||||
|
rsa_key,
|
||||||
|
public_key_der,
|
||||||
|
http_client: HttpClient::new(),
|
||||||
|
new_clients_tx: new_players_tx,
|
||||||
|
client_count: AtomicUsize::new(0),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let mut server = Server {
|
||||||
|
entities: EntityStore::new(),
|
||||||
|
worlds: WorldStore::new(shared.clone()),
|
||||||
|
chunks: ChunkStore::new(),
|
||||||
|
other: Other {
|
||||||
|
shared: shared.clone(),
|
||||||
|
tick_counter: 0,
|
||||||
|
tick_start: Instant::now(),
|
||||||
|
new_players_rx,
|
||||||
|
last_keepalive: Instant::now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
shared.handler().init(&mut server);
|
||||||
|
|
||||||
|
tokio::spawn(do_accept_loop(shared));
|
||||||
|
|
||||||
|
do_update_loop(&mut server)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_update_loop(server: &mut Server) -> ShutdownResult {
|
||||||
|
server.tick_start = Instant::now();
|
||||||
|
let shared = server.shared().clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(res) = server.0.shutdown_result.lock().take() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Ok(msg) = server.new_players_rx.try_recv() {
|
||||||
|
join_player(server, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEEPALIVE_FREQ: Duration = Duration::from_secs(8);
|
||||||
|
if server.tick_start().duration_since(server.last_keepalive) >= KEEPALIVE_FREQ {
|
||||||
|
server.last_keepalive = server.tick_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut clients = server.entities.clients_mut().unwrap();
|
||||||
|
|
||||||
|
server
|
||||||
|
.entities
|
||||||
|
.par_iter(&mut clients)
|
||||||
|
.for_each(|(e, client)| {
|
||||||
|
if let Some(client) = client.get_mut() {
|
||||||
|
client.update(e, server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
server.entities.update_old_appearances();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut chunks = server.chunks.chunks_mut().unwrap();
|
||||||
|
|
||||||
|
server.chunks.par_iter(&mut chunks).for_each(|(_, chunk)| {
|
||||||
|
chunk.apply_modifications();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.handler().update(server);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut chunks = server.chunks.chunks_mut().unwrap();
|
||||||
|
|
||||||
|
// Chunks modified this tick can have their changes applied immediately because
|
||||||
|
// they have not been observed by clients yet.
|
||||||
|
server.chunks.par_iter(&mut chunks).for_each(|(_, chunk)| {
|
||||||
|
if chunk.created_this_tick() {
|
||||||
|
chunk.clear_created_this_tick();
|
||||||
|
chunk.apply_modifications();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(
|
||||||
|
server
|
||||||
|
.0
|
||||||
|
.update_duration
|
||||||
|
.saturating_sub(server.tick_start.elapsed()),
|
||||||
|
);
|
||||||
|
server.tick_start = Instant::now();
|
||||||
|
|
||||||
|
server.tick_counter += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_player(server: &mut Server, msg: NewClientMessage) {
|
||||||
|
let (clientbound_tx, clientbound_rx) = flume::bounded(server.0.clientbound_packet_capacity);
|
||||||
|
let (serverbound_tx, serverbound_rx) = flume::bounded(server.0.serverbound_packet_capacity);
|
||||||
|
|
||||||
|
let client_packet_channels: ClientPacketChannels = (serverbound_tx, clientbound_rx);
|
||||||
|
let server_packet_channels: ServerPacketChannels = (clientbound_tx, serverbound_rx);
|
||||||
|
|
||||||
|
let _ = msg.reply.send(Ok(client_packet_channels));
|
||||||
|
|
||||||
|
let mut client = Client::new(server_packet_channels, msg.ncd.username, server);
|
||||||
|
|
||||||
|
let client_eid = match server
|
||||||
|
.entities
|
||||||
|
.create_with_uuid(Appearance::None, msg.ncd.uuid)
|
||||||
|
{
|
||||||
|
Some(eid) => eid,
|
||||||
|
None => {
|
||||||
|
log::error!(
|
||||||
|
"player '{}' cannot join the server because their UUID ({}) conflicts with an \
|
||||||
|
existing entity",
|
||||||
|
client.username(),
|
||||||
|
msg.ncd.uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
client.disconnect("Cannot join server: Your UUID conflicts with an existing entity.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut clients = server.entities.clients_mut().unwrap();
|
||||||
|
*server.entities.get(&mut clients, client_eid).unwrap() = MaybeClient(Some(Box::new(client)));
|
||||||
|
}
|
||||||
|
|
||||||
|
type Codec = (Encoder<OwnedWriteHalf>, Decoder<OwnedReadHalf>);
|
||||||
|
|
||||||
|
/// The duration of time between each sent keep alive packet.
|
||||||
|
// TODO: remove this.
|
||||||
|
|
||||||
|
async fn do_accept_loop(server: SharedServer) {
|
||||||
|
log::trace!("entering accept loop");
|
||||||
|
|
||||||
|
let listener = match TcpListener::bind(server.0.address).await {
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(e) => {
|
||||||
|
server.shutdown(Err(e).context("failed to start TCP listener"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match server.0.connection_sema.clone().acquire_owned().await {
|
||||||
|
Ok(permit) => match listener.accept().await {
|
||||||
|
Ok((stream, remote_addr)) => {
|
||||||
|
let server = server.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Setting TCP_NODELAY to true appears to trade some throughput for improved
|
||||||
|
// latency. Testing is required to determine if this is worth keeping.
|
||||||
|
if let Err(e) = stream.set_nodelay(true) {
|
||||||
|
log::error!("failed to set TCP nodelay: {e}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = handle_connection(server, stream, remote_addr).await {
|
||||||
|
log::debug!("connection to {remote_addr} ended: {e:#}");
|
||||||
|
}
|
||||||
|
drop(permit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("failed to accept incoming connection: {e}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Closed semaphore indicates server shutdown.
|
||||||
|
Err(_) => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_connection(
|
||||||
|
server: SharedServer,
|
||||||
|
stream: TcpStream,
|
||||||
|
remote_addr: SocketAddr,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let timeout = Duration::from_secs(10);
|
||||||
|
|
||||||
|
let (read, write) = stream.into_split();
|
||||||
|
let mut c: Codec = (Encoder::new(write, timeout), Decoder::new(read, timeout));
|
||||||
|
|
||||||
|
// TODO: peek stream for 0xFE legacy ping
|
||||||
|
|
||||||
|
match c.1.read_packet::<Handshake>().await?.next_state {
|
||||||
|
HandshakeNextState::Status => handle_status(server, &mut c, remote_addr)
|
||||||
|
.await
|
||||||
|
.context("error during status"),
|
||||||
|
HandshakeNextState::Login => match handle_login(&server, &mut c, remote_addr)
|
||||||
|
.await
|
||||||
|
.context("error during login")?
|
||||||
|
{
|
||||||
|
Some(npd) => handle_play(&server, c, npd)
|
||||||
|
.await
|
||||||
|
.context("error during play"),
|
||||||
|
None => Ok(()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_status(
|
||||||
|
server: SharedServer,
|
||||||
|
c: &mut Codec,
|
||||||
|
remote_addr: SocketAddr,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
c.1.read_packet::<Request>().await?;
|
||||||
|
|
||||||
|
match server
|
||||||
|
.0
|
||||||
|
.handler
|
||||||
|
.server_list_ping(&server, remote_addr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
ServerListPing::Respond {
|
||||||
|
online_players,
|
||||||
|
max_players,
|
||||||
|
description,
|
||||||
|
favicon_png,
|
||||||
|
} => {
|
||||||
|
let mut json = json!({
|
||||||
|
"version": {
|
||||||
|
"name": VERSION_NAME,
|
||||||
|
"protocol": PROTOCOL_VERSION
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"online": online_players,
|
||||||
|
"max": max_players,
|
||||||
|
// TODO: player sample?
|
||||||
|
},
|
||||||
|
"description": description,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(data) = favicon_png {
|
||||||
|
let mut buf = "data:image/png;base64,".to_string();
|
||||||
|
base64::encode_config_buf(data, base64::STANDARD, &mut buf);
|
||||||
|
json.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert("favicon".to_string(), Value::String(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
c.0.write_packet(&Response {
|
||||||
|
json_response: json.to_string(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
ServerListPing::Ignore => return Ok(()),
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ping { payload } = c.1.read_packet().await?;
|
||||||
|
|
||||||
|
c.0.write_packet(&Pong { payload }).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the login process and return the new player's data if successful.
|
||||||
|
async fn handle_login(
|
||||||
|
server: &SharedServer,
|
||||||
|
c: &mut Codec,
|
||||||
|
remote_addr: SocketAddr,
|
||||||
|
) -> anyhow::Result<Option<NewClientData>> {
|
||||||
|
let LoginStart {
|
||||||
|
username: BoundedString(username),
|
||||||
|
} = c.1.read_packet().await?;
|
||||||
|
|
||||||
|
ensure!(valid_username(&username), "invalid username '{username}'");
|
||||||
|
|
||||||
|
let (uuid, skin_blob) = if server.0.online_mode {
|
||||||
|
let verify_token: [u8; 16] = rand::random();
|
||||||
|
|
||||||
|
c.0.write_packet(&EncryptionRequest {
|
||||||
|
server_id: Default::default(), // Always empty
|
||||||
|
public_key: server.0.public_key_der.to_vec(),
|
||||||
|
verify_token: verify_token.to_vec().into(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let EncryptionResponse {
|
||||||
|
shared_secret: BoundedArray(encrypted_shared_secret),
|
||||||
|
verify_token: BoundedArray(encrypted_verify_token),
|
||||||
|
} = c.1.read_packet().await?;
|
||||||
|
|
||||||
|
let shared_secret = server
|
||||||
|
.0
|
||||||
|
.rsa_key
|
||||||
|
.decrypt(PaddingScheme::PKCS1v15Encrypt, &encrypted_shared_secret)
|
||||||
|
.context("Failed to decrypt shared secret")?;
|
||||||
|
|
||||||
|
let new_verify_token = server
|
||||||
|
.0
|
||||||
|
.rsa_key
|
||||||
|
.decrypt(PaddingScheme::PKCS1v15Encrypt, &encrypted_verify_token)
|
||||||
|
.context("Failed to decrypt verify token")?;
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
verify_token.as_slice() == new_verify_token,
|
||||||
|
"Verify tokens do not match"
|
||||||
|
);
|
||||||
|
|
||||||
|
let crypt_key: [u8; 16] = shared_secret
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.context("Shared secret has the wrong length")?;
|
||||||
|
|
||||||
|
c.0.enable_encryption(&crypt_key);
|
||||||
|
c.1.enable_encryption(&crypt_key);
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AuthResponse {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
properties: Vec<Property>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Property {
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash = Sha1::new()
|
||||||
|
.chain(&shared_secret)
|
||||||
|
.chain(&server.0.public_key_der)
|
||||||
|
.finalize();
|
||||||
|
|
||||||
|
let hex_hash = weird_hex_encoding(&hash);
|
||||||
|
|
||||||
|
let url = format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={hex_hash}&ip={}", remote_addr.ip());
|
||||||
|
let resp = server.0.http_client.get(url).send().await?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
ensure!(
|
||||||
|
status.is_success(),
|
||||||
|
"session server GET request failed: {status}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let data: AuthResponse = resp.json().await?;
|
||||||
|
|
||||||
|
ensure!(data.name == username, "usernames do not match");
|
||||||
|
|
||||||
|
let uuid = Uuid::parse_str(&data.id).context("failed to parse player's UUID")?;
|
||||||
|
|
||||||
|
let skin_blob = match data.properties.iter().find(|p| p.name == "textures") {
|
||||||
|
Some(p) => base64::decode(&p.value).context("failed to parse skin blob")?,
|
||||||
|
None => bail!("failed to find skin blob in auth response"),
|
||||||
|
};
|
||||||
|
|
||||||
|
(uuid, Some(skin_blob))
|
||||||
|
} else {
|
||||||
|
// Derive the player's UUID from a hash of their username.
|
||||||
|
let uuid = Uuid::from_slice(&Sha256::digest(&username)[..16]).unwrap();
|
||||||
|
|
||||||
|
(uuid, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let compression_threshold = 256;
|
||||||
|
c.0.write_packet(&SetCompression {
|
||||||
|
threshold: VarInt(compression_threshold as i32),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
c.0.enable_compression(compression_threshold);
|
||||||
|
c.1.enable_compression(compression_threshold);
|
||||||
|
|
||||||
|
let npd = NewClientData {
|
||||||
|
uuid,
|
||||||
|
username,
|
||||||
|
remote_addr,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !server.try_inc_player_count() {
|
||||||
|
let reason = server.0.handler.max_client_message(server, &npd).await;
|
||||||
|
log::info!("Disconnect at login: \"{reason}\"");
|
||||||
|
c.0.write_packet(&login::Disconnect { reason }).await?;
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Login::Disconnect(reason) = server.0.handler.login(server, &npd).await {
|
||||||
|
log::info!("Disconnect at login: \"{reason}\"");
|
||||||
|
c.0.write_packet(&login::Disconnect { reason }).await?;
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.0.write_packet(&LoginSuccess {
|
||||||
|
uuid: npd.uuid,
|
||||||
|
username: npd.username.clone().into(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Some(npd))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_play(server: &SharedServer, c: Codec, ncd: NewClientData) -> anyhow::Result<()> {
|
||||||
|
let (reply_tx, reply_rx) = oneshot::channel();
|
||||||
|
|
||||||
|
server
|
||||||
|
.0
|
||||||
|
.new_clients_tx
|
||||||
|
.send_async(NewClientMessage {
|
||||||
|
ncd,
|
||||||
|
reply: reply_tx,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (packet_tx, packet_rx) = match reply_rx.await {
|
||||||
|
Ok(res) => res?,
|
||||||
|
Err(_) => return Ok(()), // Server closed
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut encoder, mut decoder) = c;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Ok(pkt) = packet_rx.recv_async().await {
|
||||||
|
if let Err(e) = encoder.write_packet(&pkt).await {
|
||||||
|
log::debug!("error while sending play packet: {e:#}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let pkt = decoder.read_packet().await?;
|
||||||
|
if packet_tx.send_async(pkt).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn weird_hex_encoding(bytes: &[u8]) -> String {
|
||||||
|
BigInt::from_signed_bytes_be(bytes).to_str_radix(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn weird_hex_encoding_correct() {
|
||||||
|
assert_eq!(
|
||||||
|
weird_hex_encoding(&Sha1::digest("Notch")),
|
||||||
|
"4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
weird_hex_encoding(&Sha1::digest("jeb_")),
|
||||||
|
"-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
weird_hex_encoding(&Sha1::digest("simon")),
|
||||||
|
"88e16a1019277b15d58faf0541e11910eb756f6"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
0
src/slot.rs
Normal file
0
src/slot.rs
Normal file
527
src/text.rs
Normal file
527
src/text.rs
Normal file
|
@ -0,0 +1,527 @@
|
||||||
|
// TODO: Documentation.
|
||||||
|
// TODO: make fields of Text public?
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fmt;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use serde::de::Visitor;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
use crate::protocol::{BoundedString, Decode, Encode};
|
||||||
|
use crate::Identifier;
|
||||||
|
|
||||||
|
/// Represents formatted text in Minecraft's JSON text format.
|
||||||
|
///
|
||||||
|
/// Text is used in various places such as chat, window titles,
|
||||||
|
/// disconnect messages, written books, signs, and more.
|
||||||
|
///
|
||||||
|
/// For more information, see the relevant [Minecraft wiki article](https://minecraft.fandom.com/wiki/Raw_JSON_text_format).
|
||||||
|
///
|
||||||
|
/// Note that the current `Deserialize` implementation on this type recognizes
|
||||||
|
/// only a subset of the full JSON chat component format.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// With [`TextFormat`] in scope, you can write the following:
|
||||||
|
/// ```
|
||||||
|
/// use server::text::{Color, Text, TextFormat};
|
||||||
|
///
|
||||||
|
/// let txt = "The text is ".into_text()
|
||||||
|
/// + "Red".color(Color::RED)
|
||||||
|
/// + ", "
|
||||||
|
/// + "Green".color(Color::GREEN)
|
||||||
|
/// + ", and also "
|
||||||
|
/// + "Blue".color(Color::BLUE)
|
||||||
|
/// + "!\nAnd maybe even "
|
||||||
|
/// + "Italic".italic()
|
||||||
|
/// + ".";
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// txt.to_plain(),
|
||||||
|
/// "The text is Red, Green, and also Blue!\nAnd maybe even Italic."
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, PartialEq, Default, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Text {
|
||||||
|
#[serde(flatten)]
|
||||||
|
content: TextContent,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
color: Option<Color>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
font: Option<Cow<'static, str>>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
bold: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
italic: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
underlined: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
strikethrough: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
obfuscated: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
insertion: Option<Cow<'static, str>>,
|
||||||
|
|
||||||
|
#[serde(rename = "clickEvent")]
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
click_event: Option<ClickEvent>,
|
||||||
|
|
||||||
|
#[serde(rename = "hoverEvent")]
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
hover_event: Option<HoverEvent>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
extra: Vec<Text>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides the methods necessary for working with [`Text`] objects.
|
||||||
|
///
|
||||||
|
/// This trait exists to allow using `Into<Text>` impls without having to first
|
||||||
|
/// convert `Self` into [`Text`]. It is automatically implemented for all
|
||||||
|
/// `Into<Text>` types, including [`Text`] itself.
|
||||||
|
pub trait TextFormat: Into<Text> {
|
||||||
|
fn into_text(self) -> Text {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color(self, color: Color) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.color = Some(color);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_color(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.color = None;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font(self, font: impl Into<Cow<'static, str>>) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.font = Some(font.into());
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_font(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.font = None;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bold(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.bold = Some(true);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_bold(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.bold = Some(false);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_bold(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.bold = None;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn italic(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.italic = Some(true);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_italic(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.italic = Some(false);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_italic(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.italic = None;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlined(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.underlined = Some(true);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_underlined(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.underlined = Some(false);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_underlined(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.underlined = None;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strikethrough(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.strikethrough = Some(true);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_strikethrough(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.strikethrough = Some(false);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_strikethrough(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.strikethrough = None;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn obfuscated(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.obfuscated = Some(true);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_obfuscated(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.obfuscated = Some(false);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_obfuscated(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.obfuscated = None;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insertion(self, insertion: impl Into<Cow<'static, str>>) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.insertion = Some(insertion.into());
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_insertion(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.insertion = None;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_click_open_url(self, url: impl Into<Cow<'static, str>>) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.click_event = Some(ClickEvent::OpenUrl(url.into()));
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_click_run_command(self, command: impl Into<Cow<'static, str>>) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.click_event = Some(ClickEvent::RunCommand(command.into()));
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_click_suggest_command(self, command: impl Into<Cow<'static, str>>) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.click_event = Some(ClickEvent::SuggestCommand(command.into()));
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_click_change_page(self, page: impl Into<i32>) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.click_event = Some(ClickEvent::ChangePage(page.into()));
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_click_copy_to_clipboard(self, text: impl Into<Cow<'static, str>>) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.click_event = Some(ClickEvent::CopyToClipboard(text.into()));
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_click_event(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.click_event = None;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_hover_show_text(self, text: impl Into<Text>) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.hover_event = Some(HoverEvent::ShowText(Box::new(text.into())));
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_hover_event(self) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.hover_event = None;
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_child(self, text: impl Into<Text>) -> Text {
|
||||||
|
let mut t = self.into();
|
||||||
|
t.extra.push(text.into());
|
||||||
|
t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum TextContent {
|
||||||
|
Text { text: Cow<'static, str> },
|
||||||
|
// TODO: translate
|
||||||
|
// TODO: score
|
||||||
|
// TODO: entity names
|
||||||
|
// TODO: keybind
|
||||||
|
// TODO: nbt
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||||
|
pub struct Color {
|
||||||
|
pub r: u8,
|
||||||
|
pub g: u8,
|
||||||
|
pub b: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "action", content = "value", rename_all = "snake_case")]
|
||||||
|
enum ClickEvent {
|
||||||
|
OpenUrl(Cow<'static, str>),
|
||||||
|
/// Only usable by internal servers for security reasons.
|
||||||
|
OpenFile(Cow<'static, str>),
|
||||||
|
RunCommand(Cow<'static, str>),
|
||||||
|
SuggestCommand(Cow<'static, str>),
|
||||||
|
ChangePage(i32),
|
||||||
|
CopyToClipboard(Cow<'static, str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "action", content = "contents", rename_all = "snake_case")]
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
enum HoverEvent {
|
||||||
|
ShowText(Box<Text>),
|
||||||
|
ShowItem {
|
||||||
|
id: Identifier,
|
||||||
|
count: Option<i32>,
|
||||||
|
// TODO: tag
|
||||||
|
},
|
||||||
|
ShowEntity {
|
||||||
|
name: Box<Text>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
typ: Identifier,
|
||||||
|
// TODO: id (hyphenated entity UUID as a string)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Text {
|
||||||
|
pub fn text(plain: impl Into<Cow<'static, str>>) -> Self {
|
||||||
|
#![allow(clippy::self_named_constructors)]
|
||||||
|
Self {
|
||||||
|
content: TextContent::Text { text: plain.into() },
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_plain(&self) -> String {
|
||||||
|
let mut res = String::new();
|
||||||
|
self.write_plain(&mut res)
|
||||||
|
.expect("failed to write plain text");
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_plain(&self, w: &mut impl fmt::Write) -> fmt::Result {
|
||||||
|
if let TextContent::Text { text } = &self.content {
|
||||||
|
w.write_str(text.as_ref())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in &self.extra {
|
||||||
|
child.write_plain(w)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<Text>> TextFormat for T {}
|
||||||
|
|
||||||
|
impl<T: Into<Text>> std::ops::Add<T> for Text {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: T) -> Self::Output {
|
||||||
|
self.add_child(rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<Text>> std::ops::AddAssign<T> for Text {
|
||||||
|
fn add_assign(&mut self, rhs: T) {
|
||||||
|
self.extra.push(rhs.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Text {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Text::text(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for Text {
|
||||||
|
fn from(s: &'static str) -> Self {
|
||||||
|
Text::text(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Cow<'static, str>> for Text {
|
||||||
|
fn from(s: Cow<'static, str>) -> Self {
|
||||||
|
Text::text(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Text> for String {
|
||||||
|
fn from(t: &'a Text) -> Self {
|
||||||
|
t.to_plain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> From<&'a Text> for Cow<'b, str> {
|
||||||
|
fn from(t: &'a Text) -> Self {
|
||||||
|
String::from(t).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Text {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", String::from(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for Text {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
BoundedString::<0, 262144>(serde_json::to_string(self)?).encode(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for Text {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let string = BoundedString::<0, 262144>::decode(r)?;
|
||||||
|
Ok(serde_json::from_str(&string.0)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TextContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Text { text: "".into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
pub const AQUA: Color = Color::new(85, 255, 255);
|
||||||
|
pub const BLACK: Color = Color::new(0, 0, 0);
|
||||||
|
pub const BLUE: Color = Color::new(85, 85, 255);
|
||||||
|
pub const DARK_AQUA: Color = Color::new(0, 170, 170);
|
||||||
|
pub const DARK_BLUE: Color = Color::new(0, 0, 170);
|
||||||
|
pub const DARK_GRAY: Color = Color::new(85, 85, 85);
|
||||||
|
pub const DARK_GREEN: Color = Color::new(0, 170, 0);
|
||||||
|
pub const DARK_PURPLE: Color = Color::new(170, 0, 170);
|
||||||
|
pub const DARK_RED: Color = Color::new(170, 0, 0);
|
||||||
|
pub const GOLD: Color = Color::new(255, 170, 0);
|
||||||
|
pub const GRAY: Color = Color::new(170, 170, 170);
|
||||||
|
pub const GREEN: Color = Color::new(85, 255, 85);
|
||||||
|
pub const LIGHT_PURPLE: Color = Color::new(255, 85, 255);
|
||||||
|
pub const RED: Color = Color::new(255, 85, 85);
|
||||||
|
pub const WHITE: Color = Color::new(255, 255, 255);
|
||||||
|
pub const YELLOW: Color = Color::new(255, 255, 85);
|
||||||
|
|
||||||
|
pub const fn new(r: u8, g: u8, b: u8) -> Self {
|
||||||
|
Self { r, g, b }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Color {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b).serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Color {
|
||||||
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
deserializer.deserialize_str(ColorVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ColorVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for ColorVisitor {
|
||||||
|
type Value = Color;
|
||||||
|
|
||||||
|
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "a hex color of the form #rrggbb")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
|
||||||
|
color_from_str(s).ok_or_else(|| E::custom("invalid hex color"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color_from_str(s: &str) -> Option<Color> {
|
||||||
|
let to_num = |d| match d {
|
||||||
|
b'0'..=b'9' => Some(d - b'0'),
|
||||||
|
b'a'..=b'f' => Some(d - b'a' + 0xa),
|
||||||
|
b'A'..=b'F' => Some(d - b'A' + 0xa),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match s.as_bytes() {
|
||||||
|
[b'#', r0, r1, g0, g1, b0, b1] => Some(Color {
|
||||||
|
r: to_num(*r0)? << 4 | to_num(*r1)?,
|
||||||
|
g: to_num(*g0)? << 4 | to_num(*g1)?,
|
||||||
|
b: to_num(*b0)? << 4 | to_num(*b1)?,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_deserialize() {
|
||||||
|
let before = "foo".color(Color::RED).bold()
|
||||||
|
+ ("bar".obfuscated().color(Color::YELLOW)
|
||||||
|
+ "baz".underlined().not_bold().italic().color(Color::BLACK));
|
||||||
|
|
||||||
|
assert_eq!(before.to_plain(), "foobarbaz");
|
||||||
|
|
||||||
|
let json = serde_json::to_string_pretty(&before).unwrap();
|
||||||
|
|
||||||
|
let after: Text = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
println!("==== Before ====\n");
|
||||||
|
println!("{before:#?}");
|
||||||
|
println!("==== After ====\n");
|
||||||
|
println!("{after:#?}");
|
||||||
|
|
||||||
|
assert_eq!(before, after);
|
||||||
|
assert_eq!(before.to_plain(), after.to_plain());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color() {
|
||||||
|
assert_eq!(
|
||||||
|
color_from_str("#aBcDeF"),
|
||||||
|
Some(Color::new(0xab, 0xcd, 0xef))
|
||||||
|
);
|
||||||
|
assert_eq!(color_from_str("#fFfFfF"), Some(Color::new(255, 255, 255)));
|
||||||
|
assert_eq!(color_from_str("#00000000"), None);
|
||||||
|
assert_eq!(color_from_str("#000000"), Some(Color::BLACK));
|
||||||
|
assert_eq!(color_from_str("#"), None);
|
||||||
|
}
|
||||||
|
}
|
28
src/util.rs
Normal file
28
src/util.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
|
||||||
|
use crate::ChunkPos;
|
||||||
|
|
||||||
|
/// Returns true if the given string meets the criteria for a valid Minecraft
|
||||||
|
/// username.
|
||||||
|
pub fn valid_username(s: &str) -> bool {
|
||||||
|
(3..=16).contains(&s.len())
|
||||||
|
&& s.chars()
|
||||||
|
.all(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXTRA_RADIUS: i32 = 3;
|
||||||
|
|
||||||
|
pub fn chunks_in_view_distance(
|
||||||
|
center: ChunkPos,
|
||||||
|
distance: u8,
|
||||||
|
) -> impl FusedIterator<Item = ChunkPos> {
|
||||||
|
let dist = distance as i32 + EXTRA_RADIUS;
|
||||||
|
(center.z - dist..=center.z + dist)
|
||||||
|
.flat_map(move |z| (center.x - dist..=center.x + dist).map(move |x| ChunkPos { x, z }))
|
||||||
|
.filter(move |&p| is_chunk_in_view_distance(center, p, distance))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_chunk_in_view_distance(center: ChunkPos, other: ChunkPos, distance: u8) -> bool {
|
||||||
|
(center.x as f64 - other.x as f64).powi(2) + (center.z as f64 - other.z as f64).powi(2)
|
||||||
|
<= (distance as f64 + EXTRA_RADIUS as f64).powi(2)
|
||||||
|
}
|
124
src/var_int.rs
Normal file
124
src/var_int.rs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
// TODO: use derive_more?
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
use crate::protocol::{Decode, Encode};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
|
pub struct VarInt(pub i32);
|
||||||
|
|
||||||
|
impl VarInt {
|
||||||
|
/// The maximum number of bytes a VarInt could occupy when read from and
|
||||||
|
/// written to the Minecraft protocol.
|
||||||
|
pub const MAX_SIZE: usize = 5;
|
||||||
|
|
||||||
|
/// The number of bytes this `VarInt` will occupy when written to the
|
||||||
|
/// Minecraft protocol.
|
||||||
|
pub const fn written_size(self) -> usize {
|
||||||
|
let val = self.0 as u32;
|
||||||
|
if val & 0b11110000_00000000_00000000_00000000 != 0 {
|
||||||
|
5
|
||||||
|
} else if val & 0b11111111_11100000_00000000_00000000 != 0 {
|
||||||
|
4
|
||||||
|
} else if val & 0b11111111_11111111_11000000_00000000 != 0 {
|
||||||
|
3
|
||||||
|
} else if val & 0b11111111_11111111_11111111_10000000 != 0 {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for VarInt {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
let mut val = self.0 as u32;
|
||||||
|
loop {
|
||||||
|
if val & 0b11111111111111111111111110000000 == 0 {
|
||||||
|
w.write_u8(val as u8)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
w.write_u8(val as u8 & 0b01111111 | 0b10000000)?;
|
||||||
|
val >>= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for VarInt {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let mut val = 0;
|
||||||
|
for i in 0..Self::MAX_SIZE {
|
||||||
|
let byte = r.read_u8()?;
|
||||||
|
val |= (byte as i32 & 0b01111111) << (i * 7);
|
||||||
|
if byte & 0b10000000 == 0 {
|
||||||
|
return Ok(VarInt(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bail!("VarInt is too large")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VarInt> for i32 {
|
||||||
|
fn from(i: VarInt) -> Self {
|
||||||
|
i.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VarInt> for i64 {
|
||||||
|
fn from(i: VarInt) -> Self {
|
||||||
|
i.0 as i64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for VarInt {
|
||||||
|
fn from(i: i32) -> Self {
|
||||||
|
VarInt(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn written_size_correct() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
for n in (0..100_000)
|
||||||
|
.map(|_| rng.gen())
|
||||||
|
.chain([0, i32::MIN, i32::MAX])
|
||||||
|
.map(VarInt)
|
||||||
|
{
|
||||||
|
buf.clear();
|
||||||
|
n.encode(&mut buf).unwrap();
|
||||||
|
assert!(buf.len() == n.written_size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_decode() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
for n in (0..1_000_000)
|
||||||
|
.map(|_| rng.gen())
|
||||||
|
.chain([0, i32::MIN, i32::MAX])
|
||||||
|
{
|
||||||
|
VarInt(n).encode(&mut buf).unwrap();
|
||||||
|
|
||||||
|
let mut slice = buf.as_slice();
|
||||||
|
assert!(slice.len() <= VarInt::MAX_SIZE);
|
||||||
|
|
||||||
|
assert_eq!(n, VarInt::decode(&mut slice).unwrap().0);
|
||||||
|
|
||||||
|
assert!(slice.is_empty());
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
src/var_long.rs
Normal file
70
src/var_long.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
use crate::protocol::{Decode, Encode};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
|
pub struct VarLong(i64);
|
||||||
|
|
||||||
|
impl VarLong {
|
||||||
|
/// The maximum number of bytes a `VarLong` can occupy when read from and
|
||||||
|
/// written to the Minecraft protocol.
|
||||||
|
pub const MAX_SIZE: usize = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for VarLong {
|
||||||
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||||
|
let mut val = self.0 as u64;
|
||||||
|
loop {
|
||||||
|
if val & 0b1111111111111111111111111111111111111111111111111111111110000000 == 0 {
|
||||||
|
w.write_u8(val as u8)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
w.write_u8(val as u8 & 0b01111111 | 0b10000000)?;
|
||||||
|
val >>= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for VarLong {
|
||||||
|
fn decode(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||||
|
let mut val = 0;
|
||||||
|
for i in 0..Self::MAX_SIZE {
|
||||||
|
let byte = r.read_u8()?;
|
||||||
|
val |= (byte as i64 & 0b01111111) << (i * 7);
|
||||||
|
if byte & 0b10000000 == 0 {
|
||||||
|
return Ok(VarLong(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bail!("VarInt is too large")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_decode() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
for n in (0..1_000_000)
|
||||||
|
.map(|_| rng.gen())
|
||||||
|
.chain([0, i64::MIN, i64::MAX])
|
||||||
|
{
|
||||||
|
VarLong(n).encode(&mut buf).unwrap();
|
||||||
|
|
||||||
|
let mut slice = buf.as_slice();
|
||||||
|
assert!(slice.len() <= VarLong::MAX_SIZE);
|
||||||
|
|
||||||
|
assert_eq!(n, VarLong::decode(&mut slice).unwrap().0);
|
||||||
|
assert!(slice.is_empty());
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
251
src/world.rs
Normal file
251
src/world.rs
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
|
||||||
|
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||||
|
|
||||||
|
use crate::chunk::ChunkPos;
|
||||||
|
use crate::chunk_store::ChunkId;
|
||||||
|
use crate::component::{
|
||||||
|
ComponentStore, Components, ComponentsMut, Error, Id, IdData, IdRaw, ZippedComponents,
|
||||||
|
ZippedComponentsRaw,
|
||||||
|
};
|
||||||
|
use crate::config::DimensionId;
|
||||||
|
use crate::SharedServer;
|
||||||
|
|
||||||
|
pub struct WorldStore {
|
||||||
|
comps: ComponentStore<WorldId>,
|
||||||
|
worlds: RwLock<Vec<World>>,
|
||||||
|
shared: SharedServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorldStore {
|
||||||
|
pub(crate) fn new(shared: SharedServer) -> Self {
|
||||||
|
Self {
|
||||||
|
comps: ComponentStore::new(),
|
||||||
|
worlds: RwLock::new(Vec::new()),
|
||||||
|
shared,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(&mut self, dim: DimensionId) -> WorldId {
|
||||||
|
let height = self.shared.dimension(dim).height;
|
||||||
|
|
||||||
|
let id = self.comps.create_item();
|
||||||
|
let world = World {
|
||||||
|
chunks: HashMap::new(),
|
||||||
|
dimension: dim,
|
||||||
|
};
|
||||||
|
|
||||||
|
let idx = id.0.idx as usize;
|
||||||
|
if idx >= self.worlds.get_mut().len() {
|
||||||
|
self.worlds.get_mut().push(world);
|
||||||
|
} else {
|
||||||
|
self.worlds.get_mut()[idx] = world;
|
||||||
|
}
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a world from the server. Any [`WorldId`] referring to the
|
||||||
|
/// deleted world will be invalidated.
|
||||||
|
///
|
||||||
|
/// Note that any entities with positions inside the deleted world will not
|
||||||
|
/// be deleted themselves. These entities should be deleted or moved
|
||||||
|
/// elsewhere, preferably before calling this function.
|
||||||
|
pub fn delete(&mut self, world: WorldId) -> bool {
|
||||||
|
if self.comps.delete_item(world) {
|
||||||
|
let idx = world.0.idx as usize;
|
||||||
|
self.worlds.get_mut()[idx].chunks = HashMap::new();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.comps.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<Z>(&self, z: Z, world: WorldId) -> Option<Z::Item>
|
||||||
|
where
|
||||||
|
Z: ZippedComponents<Id = WorldId>,
|
||||||
|
{
|
||||||
|
self.comps.get(z, world)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter<'a, Z>(&'a self, z: Z) -> impl FusedIterator<Item = (WorldId, Z::Item)> + 'a
|
||||||
|
where
|
||||||
|
Z: ZippedComponents<Id = WorldId> + 'a,
|
||||||
|
{
|
||||||
|
self.comps.iter(z)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn par_iter<'a, Z>(&'a self, z: Z) -> impl ParallelIterator<Item = (WorldId, Z::Item)> + 'a
|
||||||
|
where
|
||||||
|
Z: ZippedComponents<Id = WorldId> + 'a,
|
||||||
|
{
|
||||||
|
self.comps.par_iter(z)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ids(&self) -> impl FusedIterator<Item = WorldId> + Clone + '_ {
|
||||||
|
self.comps.ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn par_ids(&self) -> impl ParallelIterator<Item = WorldId> + Clone + '_ {
|
||||||
|
self.comps.par_ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn worlds(&self) -> Result<Worlds, Error> {
|
||||||
|
Ok(Worlds {
|
||||||
|
worlds: self.worlds.try_read().ok_or(Error::NoReadAccess)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn worlds_mut(&self) -> Result<WorldsMut, Error> {
|
||||||
|
Ok(WorldsMut {
|
||||||
|
worlds: self.worlds.try_write().ok_or(Error::NoWriteAccess)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_component<C: 'static + Send + Sync + Default>(&mut self) {
|
||||||
|
self.comps.register_component::<C>();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister_component<C: 'static + Send + Sync + Default>(&mut self) {
|
||||||
|
self.comps.unregister_component::<C>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_registered<C: 'static + Send + Sync + Default>(&self) -> bool {
|
||||||
|
self.comps.is_registered::<C>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn components<C: 'static + Send + Sync + Default>(
|
||||||
|
&self,
|
||||||
|
) -> Result<Components<C, WorldId>, Error> {
|
||||||
|
self.comps.components::<C>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn components_mut<C: 'static + Send + Sync + Default>(
|
||||||
|
&self,
|
||||||
|
) -> Result<ComponentsMut<C, WorldId>, Error> {
|
||||||
|
self.comps.components_mut::<C>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct WorldId(pub(crate) IdData);
|
||||||
|
|
||||||
|
impl WorldId {
|
||||||
|
/// The value of the default [`WorldId`] which always refers to an invalid
|
||||||
|
/// world.
|
||||||
|
pub const NULL: Self = Self(IdData::NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdRaw for WorldId {
|
||||||
|
fn to_data(self) -> IdData {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_data(id: IdData) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Id for WorldId {}
|
||||||
|
|
||||||
|
pub struct Worlds<'a> {
|
||||||
|
worlds: RwLockReadGuard<'a, Vec<World>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b Worlds<'a> {
|
||||||
|
type RawItem = &'b World;
|
||||||
|
type RawIter = std::slice::Iter<'b, World>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, World>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.worlds[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.worlds.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.worlds.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b Worlds<'a> {
|
||||||
|
type Id = WorldId;
|
||||||
|
type Item = &'b World;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WorldsMut<'a> {
|
||||||
|
worlds: RwLockWriteGuard<'a, Vec<World>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b WorldsMut<'a> {
|
||||||
|
type RawItem = &'b World;
|
||||||
|
type RawIter = std::slice::Iter<'b, World>;
|
||||||
|
type RawParIter = rayon::slice::Iter<'b, World>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&self.worlds[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.worlds.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.worlds.par_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b WorldsMut<'a> {
|
||||||
|
type Id = WorldId;
|
||||||
|
type Item = &'b World;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponentsRaw for &'b mut WorldsMut<'a> {
|
||||||
|
type RawItem = &'b mut World;
|
||||||
|
type RawIter = std::slice::IterMut<'b, World>;
|
||||||
|
type RawParIter = rayon::slice::IterMut<'b, World>;
|
||||||
|
|
||||||
|
fn raw_get(self, idx: usize) -> Self::RawItem {
|
||||||
|
&mut self.worlds[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_iter(self) -> Self::RawIter {
|
||||||
|
self.worlds.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_par_iter(self) -> Self::RawParIter {
|
||||||
|
self.worlds.par_iter_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ZippedComponents for &'b mut WorldsMut<'a> {
|
||||||
|
type Id = WorldId;
|
||||||
|
type Item = &'b mut World;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct World {
|
||||||
|
chunks: HashMap<ChunkPos, ChunkId>,
|
||||||
|
dimension: DimensionId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl World {
|
||||||
|
pub fn dimension(&self) -> DimensionId {
|
||||||
|
self.dimension
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chunks(&self) -> &HashMap<ChunkPos, ChunkId> {
|
||||||
|
&self.chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chunks_mut(&mut self) -> &mut HashMap<ChunkPos, ChunkId> {
|
||||||
|
&mut self.chunks
|
||||||
|
}
|
||||||
|
}
|
9
valence-data/Cargo.toml
Normal file
9
valence-data/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "valence-data"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Generated code for the Valence project."
|
||||||
|
repository = "https://github.com/rj00a/valence"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
8
valence-data/src/lib.rs
Normal file
8
valence-data/src/lib.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
let result = 2 + 2;
|
||||||
|
assert_eq!(result, 4);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue