fix: slime spawns
This commit is contained in:
parent
f6d0b4ab20
commit
09790fc093
295
Cargo.lock
generated
295
Cargo.lock
generated
@ -832,35 +832,6 @@ version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"document-features",
|
||||
"idna",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpp_demangle"
|
||||
version = "0.5.1"
|
||||
@ -1444,15 +1415,6 @@ dependencies = [
|
||||
"syn 3.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.9"
|
||||
@ -1824,8 +1786,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1835,9 +1799,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"rand_core 0.10.1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2096,6 +2062,22 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-timeout"
|
||||
version = "0.5.2"
|
||||
@ -2115,13 +2097,16 @@ version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
@ -2466,12 +2451,6 @@ version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@ -2487,6 +2466,12 @@ version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lz4-java-wrc"
|
||||
version = "0.2.0"
|
||||
@ -3346,6 +3331,7 @@ dependencies = [
|
||||
"pumpkin-world",
|
||||
"rand",
|
||||
"rayon",
|
||||
"reqwest",
|
||||
"rsa",
|
||||
"rustc-hash",
|
||||
"rustc_version_runtime",
|
||||
@ -3364,7 +3350,6 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-serde-structured",
|
||||
"tracing-subscriber",
|
||||
"ureq",
|
||||
"uuid",
|
||||
"wasm-encoder 0.257.1",
|
||||
"wasmparser 0.257.1",
|
||||
@ -3455,6 +3440,7 @@ dependencies = [
|
||||
"pumpkin-util",
|
||||
"pumpkin-world",
|
||||
"rand",
|
||||
"rustc-hash",
|
||||
"thiserror 2.0.20",
|
||||
"tracing",
|
||||
]
|
||||
@ -3502,12 +3488,12 @@ name = "pumpkin-plugin-utils"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"pumpkin-plugin-api",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.20",
|
||||
"tracing",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3554,13 +3540,13 @@ dependencies = [
|
||||
"pumpkin-codecs",
|
||||
"pumpkin-nbt",
|
||||
"quote",
|
||||
"reqwest",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"syn 3.0.4",
|
||||
"thiserror 2.0.20",
|
||||
"ureq",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@ -3605,6 +3591,62 @@ version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp 0.5.15",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.4.3",
|
||||
"lru-slab",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.6.1"
|
||||
@ -3668,6 +3710,15 @@ version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_pcg"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||
dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
@ -3775,6 +3826,47 @@ dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
@ -4217,6 +4309,7 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@ -4262,6 +4355,12 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a252f5e20f038fe7b4ea53e073e65398d652c864cc162fc77c56c2f13717b888"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@ -4385,6 +4484,18 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serdect"
|
||||
version = "0.4.3"
|
||||
@ -4631,6 +4742,9 @@ name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
@ -4986,6 +5100,24 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@ -5121,37 +5253,6 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"cookie_store",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@ -5164,12 +5265,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-zero"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@ -5245,6 +5340,16 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.127"
|
||||
@ -5656,6 +5761,26 @@ dependencies = [
|
||||
"wasmtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.9"
|
||||
@ -5678,7 +5803,7 @@ dependencies = [
|
||||
"event-listener",
|
||||
"futures",
|
||||
"log",
|
||||
"quinn-udp",
|
||||
"quinn-udp 0.6.1",
|
||||
"rtc",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@ -207,7 +207,7 @@ time = { version = "0.3", default-features = false, features = ["std"] }
|
||||
arc-swap = { version = "1.9", default-features = false }
|
||||
tokio-util = { version = "0.7.19", default-features = false }
|
||||
toml = { version = "1.1", default-features = false, features = ["parse", "display", "serde"] }
|
||||
ureq = { version = "3.4.0", default-features = false, features = ["rustls"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "http2"] }
|
||||
notify = { version = "8.2.0", default-features = false, features = ["macos_fsevent"] }
|
||||
|
||||
sysinfo = { version = "0.39", default-features = false, features = ["system"] }
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,7 @@ pumpkin-util.workspace = true
|
||||
rand.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
rustc-hash.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@ -58,7 +58,7 @@ pub mod stonecutter_screen_handler;
|
||||
pub mod sync_handler;
|
||||
pub mod window_property;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
pub use error::InventoryError;
|
||||
use pumpkin_data::data_component_impl::EquipmentSlot;
|
||||
@ -71,10 +71,10 @@ use crate::player::player_inventory::PlayerInventory;
|
||||
/// (head, chest, legs, feet, off-hand) used by the player screen handler.
|
||||
///
|
||||
/// # Returns
|
||||
/// A `HashMap` where keys are slot indices and values are the corresponding [`EquipmentSlot`]s.
|
||||
/// A `FxHashMap` where keys are slot indices and values are the corresponding [`EquipmentSlot`]s.
|
||||
#[must_use]
|
||||
pub fn build_equipment_slots() -> HashMap<usize, EquipmentSlot> {
|
||||
let mut equipment_slots = HashMap::new();
|
||||
pub fn build_equipment_slots() -> FxHashMap<usize, EquipmentSlot> {
|
||||
let mut equipment_slots = FxHashMap::default();
|
||||
equipment_slots.insert(
|
||||
EquipmentSlot::FEET.get_offset_entity_slot_id(PlayerInventory::MAIN_SIZE as i32) as usize,
|
||||
EquipmentSlot::FEET,
|
||||
@ -92,7 +92,6 @@ pub fn build_equipment_slots() -> HashMap<usize, EquipmentSlot> {
|
||||
EquipmentSlot::HEAD,
|
||||
);
|
||||
|
||||
equipment_slots.insert(PlayerInventory::OFF_HAND_SLOT, EquipmentSlot::OFF_HAND);
|
||||
equipment_slots.insert(PlayerInventory::OFF_HAND_SLOT, EquipmentSlot::OFF_HAND);
|
||||
equipment_slots
|
||||
}
|
||||
|
||||
@ -492,7 +492,6 @@ impl Slot for MerchantResultSlot {
|
||||
mod tests {
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
sync::atomic::{AtomicI32, AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
@ -627,7 +626,7 @@ mod tests {
|
||||
(
|
||||
Arc::new(PlayerInventory::new(
|
||||
Arc::new(Mutex::new(EntityEquipment::new())),
|
||||
Arc::new(HashMap::new()),
|
||||
Arc::new(rustc_hash::FxHashMap::default()),
|
||||
)),
|
||||
Arc::new(SimpleInventory::new(3)),
|
||||
)
|
||||
|
||||
@ -16,8 +16,8 @@ use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_protocol::java::client::play::CSetPlayerInventory;
|
||||
use pumpkin_util::Hand;
|
||||
use pumpkin_world::inventory::{Clearable, Inventory};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
@ -35,7 +35,7 @@ pub struct PlayerInventory {
|
||||
/// Mapping of slot indices to equipment slot types.
|
||||
///
|
||||
/// Used to identify which slots correspond to armor and off-hand equipment.
|
||||
pub equipment_slots: Arc<HashMap<usize, EquipmentSlot>>,
|
||||
pub equipment_slots: Arc<FxHashMap<usize, EquipmentSlot>>,
|
||||
/// The currently selected hotbar slot index (0-8).
|
||||
pub selected_slot: AtomicU8,
|
||||
/// The entity equipment storage for armor and off-hand items.
|
||||
@ -60,7 +60,7 @@ impl PlayerInventory {
|
||||
// TODO: Add inventory load from nbt
|
||||
pub fn new(
|
||||
entity_equipment: Arc<Mutex<EntityEquipment>>,
|
||||
equipment_slots: Arc<HashMap<usize, EquipmentSlot>>,
|
||||
equipment_slots: Arc<FxHashMap<usize, EquipmentSlot>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
main_inventory: RwLock::new(std::array::from_fn(|_| ItemStack::EMPTY.clone())),
|
||||
|
||||
@ -11,7 +11,7 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
ureq = { workspace = true, features = ["json"] }
|
||||
reqwest = { workspace = true, features = ["blocking", "json"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@ -18,7 +18,7 @@ pub enum HttpError {
|
||||
|
||||
/// Helper client for querying Pumpkin marketplace REST APIs.
|
||||
pub struct HttpClient {
|
||||
user_agent: String,
|
||||
client: reqwest::blocking::Client,
|
||||
}
|
||||
|
||||
impl Default for HttpClient {
|
||||
@ -31,9 +31,11 @@ impl HttpClient {
|
||||
/// Creates a new HTTP client with the specified User-Agent header.
|
||||
#[must_use]
|
||||
pub fn new(user_agent: &str) -> Self {
|
||||
Self {
|
||||
user_agent: user_agent.to_string(),
|
||||
}
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
.build()
|
||||
.unwrap_or_default();
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Performs an HTTP GET request and returns the response body as a string.
|
||||
@ -42,24 +44,23 @@ impl HttpClient {
|
||||
///
|
||||
/// Returns `HttpError` if the request fails or returns a non-2xx status code.
|
||||
pub fn get(&self, url: &str) -> Result<String, HttpError> {
|
||||
let mut response = ureq::get(url)
|
||||
.header("User-Agent", &self.user_agent)
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("Accept", "application/json")
|
||||
.call()
|
||||
.send()
|
||||
.map_err(|e| HttpError::RequestFailed(e.to_string()))?;
|
||||
|
||||
let status = response.status().as_u16();
|
||||
if status < 200 || status >= 300 {
|
||||
let body = response
|
||||
.body_mut()
|
||||
.read_to_string()
|
||||
.text()
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
return Err(HttpError::BadStatus(status, body));
|
||||
}
|
||||
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string()
|
||||
.text()
|
||||
.map_err(|e| HttpError::BodyRead(e.to_string()))
|
||||
}
|
||||
|
||||
@ -69,25 +70,25 @@ impl HttpClient {
|
||||
///
|
||||
/// Returns `HttpError` if the request fails or returns a non-2xx status code.
|
||||
pub fn post_json(&self, url: &str, json_payload: &str) -> Result<String, HttpError> {
|
||||
let mut response = ureq::post(url)
|
||||
.header("User-Agent", &self.user_agent)
|
||||
let response = self
|
||||
.client
|
||||
.post(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.send(json_payload)
|
||||
.body(json_payload.to_string())
|
||||
.send()
|
||||
.map_err(|e| HttpError::RequestFailed(e.to_string()))?;
|
||||
|
||||
let status = response.status().as_u16();
|
||||
if status < 200 || status >= 300 {
|
||||
let body = response
|
||||
.body_mut()
|
||||
.read_to_string()
|
||||
.text()
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
return Err(HttpError::BadStatus(status, body));
|
||||
}
|
||||
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string()
|
||||
.text()
|
||||
.map_err(|e| HttpError::BodyRead(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ ecdsa.workspace = true
|
||||
rsa.workspace = true
|
||||
sha2.workspace = true
|
||||
crypto-bigint.workspace = true
|
||||
ureq.workspace = true
|
||||
reqwest.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true, features = ["html_reports"] }
|
||||
|
||||
@ -244,32 +244,26 @@ pub const OIDC_DISCOVERY_URL: &str =
|
||||
"https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/1.0.0.0";
|
||||
|
||||
/// Fetches the OIDC JSON Web Key Set (JWKS) from the discovery endpoint.
|
||||
pub fn fetch_oidc_jwks(
|
||||
pub async fn fetch_oidc_jwks(
|
||||
discovery_url: Option<&str>,
|
||||
connect_timeout_ms: u32,
|
||||
read_timeout_ms: u32,
|
||||
) -> Result<(String, Jwks), AuthError> {
|
||||
let url = discovery_url.unwrap_or(OIDC_DISCOVERY_URL);
|
||||
let config = ureq::Agent::config_builder()
|
||||
.timeout_connect(Some(std::time::Duration::from_millis(
|
||||
connect_timeout_ms as u64,
|
||||
)))
|
||||
.timeout_recv_response(Some(std::time::Duration::from_millis(
|
||||
read_timeout_ms as u64,
|
||||
)))
|
||||
.build();
|
||||
let agent: ureq::Agent = config.into();
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_millis(connect_timeout_ms as u64))
|
||||
.timeout(std::time::Duration::from_millis(read_timeout_ms as u64))
|
||||
.build()
|
||||
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?;
|
||||
|
||||
let discovery: Value = agent
|
||||
let discovery: Value = client
|
||||
.get(url)
|
||||
.call()
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
|
||||
.body_mut()
|
||||
.read_to_string()
|
||||
.map_err(|e| AuthError::PublicKeyBuild(format!("Failed to read response: {e}")))
|
||||
.and_then(|s| {
|
||||
serde_json::from_str(&s).map_err(|e| AuthError::PublicKeyBuild(e.to_string()))
|
||||
})?;
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?;
|
||||
|
||||
let service_uri = discovery
|
||||
.get("result")
|
||||
@ -281,16 +275,14 @@ pub fn fetch_oidc_jwks(
|
||||
.ok_or_else(|| AuthError::PublicKeyBuild("Discovery missing serviceUri".into()))?;
|
||||
|
||||
let openid_config_url = format!("{service_uri}/.well-known/openid-configuration");
|
||||
let openid_config: Value = agent
|
||||
let openid_config: Value = client
|
||||
.get(&openid_config_url)
|
||||
.call()
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
|
||||
.body_mut()
|
||||
.read_to_string()
|
||||
.map_err(|e| AuthError::PublicKeyBuild(format!("Failed to read response: {e}")))
|
||||
.and_then(|s| {
|
||||
serde_json::from_str(&s).map_err(|e| AuthError::PublicKeyBuild(e.to_string()))
|
||||
})?;
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?;
|
||||
|
||||
let jwks_uri = openid_config
|
||||
.get("jwks_uri")
|
||||
@ -303,16 +295,14 @@ pub fn fetch_oidc_jwks(
|
||||
.ok_or_else(|| AuthError::PublicKeyBuild("OpenID config missing issuer".into()))?
|
||||
.to_string();
|
||||
|
||||
let jwks: Jwks = agent
|
||||
let jwks: Jwks = client
|
||||
.get(jwks_uri)
|
||||
.call()
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
|
||||
.body_mut()
|
||||
.read_to_string()
|
||||
.map_err(|e| AuthError::PublicKeyBuild(format!("Failed to read response: {e}")))
|
||||
.and_then(|s| {
|
||||
serde_json::from_str(&s).map_err(|e| AuthError::PublicKeyBuild(e.to_string()))
|
||||
})?;
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?;
|
||||
|
||||
Ok((issuer, jwks))
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::io::{ErrorKind, Read};
|
||||
use std::marker::PhantomData;
|
||||
use std::path::PathBuf;
|
||||
@ -141,15 +141,15 @@ impl ChunkBitmap {
|
||||
// Repeated: u8 key_len, key bytes, u32 value
|
||||
// Terminated by a single 0x00 byte.
|
||||
|
||||
struct NbtFeatures(HashMap<String, u32>);
|
||||
struct NbtFeatures(FxHashMap<String, u32>);
|
||||
|
||||
impl NbtFeatures {
|
||||
fn empty() -> Self {
|
||||
Self(HashMap::new())
|
||||
Self(FxHashMap::default())
|
||||
}
|
||||
|
||||
fn from_bytes(buf: &mut impl Buf) -> Result<Self, ChunkReadingError> {
|
||||
let mut map = HashMap::new();
|
||||
let mut map = FxHashMap::default();
|
||||
loop {
|
||||
if !buf.has_remaining() {
|
||||
return Err(ChunkReadingError::IoError(std::io::Error::from(
|
||||
|
||||
@ -845,28 +845,31 @@ impl LightContainer {
|
||||
matches!(self, Self::Empty(_))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn index(x: usize, y: usize, z: usize) -> usize {
|
||||
y * 16 * 16 + z * 16 + x
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn get(&self, x: usize, y: usize, z: usize) -> u8 {
|
||||
match self {
|
||||
Self::Full(data) => {
|
||||
let index = Self::index(x, y, z);
|
||||
data[index >> 1] >> (4 * (index & 1)) & 0x0F
|
||||
(data[index >> 1] >> (4 * (index & 1))) & 0x0F
|
||||
}
|
||||
Self::Empty(default) => *default,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set(&mut self, x: usize, y: usize, z: usize, value: u8) {
|
||||
match self {
|
||||
Self::Full(data) => {
|
||||
let index = Self::index(x, y, z);
|
||||
let mask = 0x0F << (4 * (index & 1));
|
||||
data[index >> 1] &= !mask;
|
||||
data[index >> 1] |= value << (4 * (index & 1));
|
||||
let shift = 4 * (index & 1);
|
||||
let mask = 0x0F << shift;
|
||||
data[index >> 1] = (data[index >> 1] & !mask) | (value << shift);
|
||||
}
|
||||
Self::Empty(default) => {
|
||||
if value != *default {
|
||||
@ -877,6 +880,39 @@ impl LightContainer {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_column_y_range(
|
||||
&mut self,
|
||||
x: usize,
|
||||
z: usize,
|
||||
y_start: usize,
|
||||
y_end: usize,
|
||||
value: u8,
|
||||
) {
|
||||
if y_start >= y_end {
|
||||
return;
|
||||
}
|
||||
match self {
|
||||
Self::Full(data) => {
|
||||
let shift = 4 * (x & 1);
|
||||
let mask = 0x0F << shift;
|
||||
let val = (value & 0x0F) << shift;
|
||||
let mut byte_idx = (y_start * 256 + z * 16 + x) >> 1;
|
||||
for _ in y_start..y_end {
|
||||
data[byte_idx] = (data[byte_idx] & !mask) | val;
|
||||
byte_idx += 128;
|
||||
}
|
||||
}
|
||||
Self::Empty(default) => {
|
||||
if value != *default {
|
||||
*self = Self::new_filled(*default);
|
||||
self.set_column_y_range(x, z, y_start, y_end, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn fill(&mut self, value: u8) {
|
||||
*self = Self::new_filled(value);
|
||||
}
|
||||
|
||||
@ -242,41 +242,82 @@ impl GenerationSchedule {
|
||||
chunk_map: &HashMap<ChunkPos, ChunkHolder>,
|
||||
last_level: &ChunkLevel,
|
||||
last_high_priority: &[ChunkPos],
|
||||
waiting_for_chunks: &HashSetType<NodeKey>,
|
||||
_waiting_for_chunks: &HashSetType<NodeKey>,
|
||||
) -> usize {
|
||||
debug_assert!(queue.is_empty());
|
||||
|
||||
let mut to_drop = Vec::new();
|
||||
let mut ready = Vec::new();
|
||||
for (key, node) in &mut graph.nodes {
|
||||
if node.stage == StagedChunkEnum::None
|
||||
|| node.in_degree != 0
|
||||
|| node.in_queue
|
||||
|| node.in_flight
|
||||
|| waiting_for_chunks.contains(&key)
|
||||
{
|
||||
|
||||
for (key, node) in &graph.nodes {
|
||||
if node.stage == StagedChunkEnum::None || node.in_flight {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(holder) = chunk_map.get(&node.pos) else {
|
||||
to_drop.push(key);
|
||||
continue;
|
||||
};
|
||||
if holder.current_stage >= node.stage || holder.tasks[node.stage as usize] != key {
|
||||
|
||||
let effective_target = holder.target_stage.max(holder.dependency_stage);
|
||||
if holder.current_stage >= node.stage
|
||||
|| holder.tasks[node.stage as usize] != key
|
||||
|| node.stage > effective_target
|
||||
{
|
||||
to_drop.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if node.in_degree == 0 && !node.in_queue {
|
||||
ready.push((key, node.pos, node.stage));
|
||||
}
|
||||
|
||||
for (key, pos, stage) in &ready {
|
||||
let Some(node) = graph.nodes.get_mut(*key) else {
|
||||
continue;
|
||||
};
|
||||
node.in_queue = true;
|
||||
queue.push(TaskHeapNode(
|
||||
Self::calc_priority(last_level, last_high_priority, *pos, *stage),
|
||||
*key,
|
||||
));
|
||||
}
|
||||
|
||||
ready.len()
|
||||
for key in to_drop {
|
||||
let Some(old) = graph.nodes.remove(key) else {
|
||||
continue;
|
||||
};
|
||||
let mut edge = old.edge;
|
||||
while !edge.is_null() {
|
||||
let Some(cur) = graph.edges.remove(edge) else {
|
||||
break;
|
||||
};
|
||||
if let Some(target_node) = graph.nodes.get_mut(cur.to)
|
||||
&& target_node.in_degree > 0
|
||||
{
|
||||
target_node.in_degree -= 1;
|
||||
if target_node.in_degree == 0 && !target_node.in_queue && !target_node.in_flight
|
||||
{
|
||||
target_node.in_queue = true;
|
||||
queue.push(TaskHeapNode(
|
||||
Self::calc_priority(
|
||||
last_level,
|
||||
last_high_priority,
|
||||
target_node.pos,
|
||||
target_node.stage,
|
||||
),
|
||||
cur.to,
|
||||
));
|
||||
}
|
||||
}
|
||||
edge = cur.next;
|
||||
}
|
||||
}
|
||||
|
||||
for (key, pos, stage) in ready {
|
||||
if let Some(node) = graph.nodes.get_mut(key)
|
||||
&& node.in_degree == 0
|
||||
&& !node.in_queue
|
||||
{
|
||||
node.in_queue = true;
|
||||
queue.push(TaskHeapNode(
|
||||
Self::calc_priority(last_level, last_high_priority, pos, stage),
|
||||
key,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
queue.len()
|
||||
}
|
||||
|
||||
/// Ensure that the dependency chain for `req_stage` exists on `holder` (for chunk at
|
||||
@ -296,7 +337,7 @@ impl GenerationSchedule {
|
||||
chunk_pos: ChunkPos,
|
||||
holder: &mut ChunkHolder,
|
||||
req_stage: StagedChunkEnum,
|
||||
) {
|
||||
) -> [bool; StagedChunkEnum::COUNT] {
|
||||
// Insert occupied_by edge head
|
||||
holder.occupied_by = graph.edges.insert(crate::chunk_system::dag::Edge::new(
|
||||
dependency_task,
|
||||
@ -317,6 +358,7 @@ impl GenerationSchedule {
|
||||
|
||||
// Effective target is the max of what the player wants and what dependencies need.
|
||||
let effective_target = holder.target_stage.max(holder.dependency_stage);
|
||||
let mut newly_created = [false; StagedChunkEnum::COUNT];
|
||||
|
||||
// Create any missing tasks from current_stage+1 up to effective_target.
|
||||
// We do this even when current_stage >= req_stage, because dependency_stage may
|
||||
@ -325,7 +367,6 @@ impl GenerationSchedule {
|
||||
let empty = StagedChunkEnum::Empty as usize;
|
||||
let start = (holder.current_stage as usize + 1).max(empty);
|
||||
let end = effective_target as u8 as usize;
|
||||
let mut newly_created = [false; StagedChunkEnum::COUNT];
|
||||
|
||||
for (i, flag) in newly_created[start..=end].iter_mut().enumerate() {
|
||||
let stage_i = start + i;
|
||||
@ -385,7 +426,7 @@ impl GenerationSchedule {
|
||||
// it was only blocked on `occupied` (handled above) and the stage itself is done.
|
||||
// Do NOT add an edge here: tasks[req_stage] is null (completed and dropped).
|
||||
if holder.current_stage >= req_stage {
|
||||
return;
|
||||
return newly_created;
|
||||
}
|
||||
|
||||
// Wire req_stage task → dependency_task so dependency_task can't run until
|
||||
@ -400,6 +441,8 @@ impl GenerationSchedule {
|
||||
"holder.tasks[req_stage] must not be null before adding edge"
|
||||
);
|
||||
graph.add_edge(ano_task, dependency_task);
|
||||
|
||||
newly_created
|
||||
}
|
||||
|
||||
/// Check if any tasks parked in `waiting_for_chunks` now have all their neighbor
|
||||
@ -411,11 +454,27 @@ impl GenerationSchedule {
|
||||
}
|
||||
|
||||
let mut now_ready: Vec<NodeKey> = Vec::new();
|
||||
let mut stale: Vec<NodeKey> = Vec::new();
|
||||
|
||||
self.waiting_for_chunks.retain(|&node_key| {
|
||||
let Some(node) = self.graph.nodes.get(node_key) else {
|
||||
return false; // node was dropped, discard silently
|
||||
};
|
||||
|
||||
let Some(holder) = self.chunk_map.get(&node.pos) else {
|
||||
stale.push(node_key);
|
||||
return false;
|
||||
};
|
||||
|
||||
let effective_target = holder.target_stage.max(holder.dependency_stage);
|
||||
if holder.current_stage >= node.stage
|
||||
|| holder.tasks[node.stage as usize] != node_key
|
||||
|| node.stage > effective_target
|
||||
{
|
||||
stale.push(node_key);
|
||||
return false;
|
||||
}
|
||||
|
||||
let read_radius = node.stage.get_read_radius();
|
||||
let pos = node.pos;
|
||||
let all_ready = (-read_radius..=read_radius).all(|dx| {
|
||||
@ -437,6 +496,10 @@ impl GenerationSchedule {
|
||||
}
|
||||
});
|
||||
|
||||
for node_key in stale {
|
||||
self.drop_node(node_key);
|
||||
}
|
||||
|
||||
for node_key in now_ready {
|
||||
if let Some(n) = self.graph.nodes.get_mut(node_key)
|
||||
&& n.in_degree == 0
|
||||
@ -462,6 +525,9 @@ impl GenerationSchedule {
|
||||
let Some(new_level) = new_data.0 else {
|
||||
return true;
|
||||
};
|
||||
let mut worklist: std::collections::VecDeque<(ChunkPos, StagedChunkEnum, NodeKey)> =
|
||||
std::collections::VecDeque::new();
|
||||
|
||||
for (pos, (old_stage, new_stage)) in new_level.0 {
|
||||
debug_assert_ne!(old_stage, new_stage);
|
||||
debug_assert_eq!(
|
||||
@ -537,7 +603,7 @@ impl GenerationSchedule {
|
||||
let new_pos = pos.add_raw(dx, dz);
|
||||
let req_stage = dependency[dx.abs().max(dz.abs()) as usize];
|
||||
if new_pos == pos {
|
||||
Self::ensure_dependency_chain(
|
||||
let newly_created = Self::ensure_dependency_chain(
|
||||
&mut self.graph,
|
||||
&mut self.queue,
|
||||
&self.last_level,
|
||||
@ -547,20 +613,33 @@ impl GenerationSchedule {
|
||||
&mut holder,
|
||||
req_stage,
|
||||
);
|
||||
for (stage_i, &created) in newly_created.iter().enumerate() {
|
||||
if created && stage_i > 1 {
|
||||
let stage = StagedChunkEnum::from(stage_i as u8);
|
||||
let dependency = stage.get_direct_dependencies();
|
||||
let radius = stage.get_direct_radius();
|
||||
let cur_task = holder.tasks[stage_i];
|
||||
for ndx in -radius..=radius {
|
||||
for ndz in -radius..=radius {
|
||||
if ndx == 0 && ndz == 0 {
|
||||
continue;
|
||||
}
|
||||
let neighbor_pos = new_pos.add_raw(ndx, ndz);
|
||||
let neighbor_req = dependency
|
||||
[ndx.abs().max(ndz.abs()) as usize];
|
||||
worklist.push_back((
|
||||
neighbor_pos,
|
||||
neighbor_req,
|
||||
cur_task,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let ano_chunk = self.chunk_map.entry(new_pos).or_default();
|
||||
Self::ensure_dependency_chain(
|
||||
&mut self.graph,
|
||||
&mut self.queue,
|
||||
&self.last_level,
|
||||
&self.last_high_priority,
|
||||
task,
|
||||
new_pos,
|
||||
ano_chunk,
|
||||
req_stage,
|
||||
);
|
||||
worklist.push_back((new_pos, req_stage, task));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -573,6 +652,40 @@ impl GenerationSchedule {
|
||||
}
|
||||
self.chunk_map.insert(pos, holder);
|
||||
}
|
||||
|
||||
while let Some((pos, req_stage, dep_task)) = worklist.pop_front() {
|
||||
let mut holder = self.chunk_map.remove(&pos).unwrap_or_default();
|
||||
let newly_created = Self::ensure_dependency_chain(
|
||||
&mut self.graph,
|
||||
&mut self.queue,
|
||||
&new_level.1,
|
||||
&self.last_high_priority,
|
||||
dep_task,
|
||||
pos,
|
||||
&mut holder,
|
||||
req_stage,
|
||||
);
|
||||
for (stage_i, &created) in newly_created.iter().enumerate() {
|
||||
if created && stage_i > 1 {
|
||||
let stage = StagedChunkEnum::from(stage_i as u8);
|
||||
let dependency = stage.get_direct_dependencies();
|
||||
let radius = stage.get_direct_radius();
|
||||
let cur_task = holder.tasks[stage_i];
|
||||
for dx in -radius..=radius {
|
||||
for dz in -radius..=radius {
|
||||
if dx == 0 && dz == 0 {
|
||||
continue;
|
||||
}
|
||||
let neighbor_pos = pos.add_raw(dx, dz);
|
||||
let neighbor_req = dependency[dx.abs().max(dz.abs()) as usize];
|
||||
worklist.push_back((neighbor_pos, neighbor_req, cur_task));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.chunk_map.insert(pos, holder);
|
||||
}
|
||||
|
||||
self.last_level = new_level.1;
|
||||
self.queue_dirty = true;
|
||||
true
|
||||
@ -852,9 +965,6 @@ impl GenerationSchedule {
|
||||
debug_assert!(node.in_degree >= 1);
|
||||
node.in_degree -= 1;
|
||||
if node.in_degree == 0 && !node.in_queue {
|
||||
// Don't queue if parked in waiting_for_chunks — check_waiting_tasks()
|
||||
// will re-queue it once chunk data arrives.
|
||||
if !self.waiting_for_chunks.contains(&cur.to) {
|
||||
self.queue.push(TaskHeapNode(
|
||||
Self::calc_priority(
|
||||
&self.last_level,
|
||||
@ -867,7 +977,6 @@ impl GenerationSchedule {
|
||||
node.in_queue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
edge = cur.next;
|
||||
}
|
||||
}
|
||||
@ -933,15 +1042,6 @@ impl GenerationSchedule {
|
||||
}
|
||||
}
|
||||
}
|
||||
// If this chunk was only loaded for a dependency or cancelled
|
||||
// and is no longer needed, clear dependency_stage and queue unload.
|
||||
if holder.target_stage == StagedChunkEnum::None
|
||||
&& holder.current_stage >= holder.dependency_stage
|
||||
{
|
||||
holder.dependency_stage = StagedChunkEnum::None;
|
||||
self.unload_chunks.insert(pos);
|
||||
}
|
||||
|
||||
holder.chunk = Some(chunk);
|
||||
self.chunk_map.insert(pos, holder);
|
||||
|
||||
@ -1021,15 +1121,6 @@ impl GenerationSchedule {
|
||||
}
|
||||
holder.occupied = NodeKey::null();
|
||||
|
||||
// If this chunk was only loaded for a dependency or cancelled
|
||||
// and is no longer needed, clear dependency_stage and queue unload.
|
||||
if holder.target_stage == StagedChunkEnum::None
|
||||
&& holder.current_stage >= holder.dependency_stage
|
||||
{
|
||||
holder.dependency_stage = StagedChunkEnum::None;
|
||||
self.unload_chunks.insert(new_pos);
|
||||
}
|
||||
|
||||
self.chunk_map.insert(new_pos, holder);
|
||||
}
|
||||
Chunk::Proto(chunk) => {
|
||||
@ -1056,14 +1147,6 @@ impl GenerationSchedule {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear dependency_stage and queue unload if no longer needed
|
||||
if holder.target_stage == StagedChunkEnum::None
|
||||
&& holder.current_stage >= holder.dependency_stage
|
||||
{
|
||||
holder.dependency_stage = StagedChunkEnum::None;
|
||||
self.unload_chunks.insert(new_pos);
|
||||
}
|
||||
|
||||
holder.occupied = NodeKey::null();
|
||||
holder.chunk = Some(Chunk::Proto(chunk));
|
||||
self.chunk_map.insert(new_pos, holder);
|
||||
|
||||
@ -4,34 +4,73 @@ use crate::generation::height_limit::HeightLimitView;
|
||||
use crate::generation::proto_chunk::GenerationCache;
|
||||
use crate::lighting::storage::{get_block_light, get_sky_light, set_block_light, set_sky_light};
|
||||
use pumpkin_config::lighting::LightingEngineConfig;
|
||||
use pumpkin_data::BlockDirection;
|
||||
use pumpkin_data::{BlockDirection, BlockStateId};
|
||||
use pumpkin_util::HeightMap;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use std::collections::VecDeque;
|
||||
//use std::time::Instant;
|
||||
|
||||
// These are hit on every neighbour of every propagated block, so the hash
|
||||
// function dominates. Use rustc-hash's fast hasher instead of the std default
|
||||
// (SipHash), which is what "Fast" was meant to imply.
|
||||
type FastHashSet<K> = rustc_hash::FxHashSet<K>;
|
||||
type FastHashMap<K, V> = rustc_hash::FxHashMap<K, V>;
|
||||
use crate::ProtoChunk;
|
||||
|
||||
/// Trait to unify Block and Sky light logic
|
||||
pub trait LightProvider {
|
||||
fn get_light(cache: &Cache, pos: BlockPos) -> u8;
|
||||
fn set_light(cache: &mut Cache, pos: BlockPos, level: u8);
|
||||
fn get_light_proto(
|
||||
chunk: &ProtoChunk,
|
||||
section_idx: usize,
|
||||
lx: usize,
|
||||
ly: usize,
|
||||
lz: usize,
|
||||
) -> u8;
|
||||
fn set_light_proto(
|
||||
chunk: &mut ProtoChunk,
|
||||
section_idx: usize,
|
||||
lx: usize,
|
||||
ly: usize,
|
||||
lz: usize,
|
||||
level: u8,
|
||||
);
|
||||
fn propagate_level(current_level: u8, opacity: u8, dir: BlockDirection) -> u8;
|
||||
}
|
||||
|
||||
pub struct BlockLightProvider;
|
||||
impl LightProvider for BlockLightProvider {
|
||||
#[inline]
|
||||
fn get_light(cache: &Cache, pos: BlockPos) -> u8 {
|
||||
get_block_light(cache, pos)
|
||||
}
|
||||
#[inline]
|
||||
fn set_light(cache: &mut Cache, pos: BlockPos, level: u8) {
|
||||
set_block_light(cache, pos, level);
|
||||
}
|
||||
#[inline]
|
||||
fn get_light_proto(
|
||||
chunk: &ProtoChunk,
|
||||
section_idx: usize,
|
||||
lx: usize,
|
||||
ly: usize,
|
||||
lz: usize,
|
||||
) -> u8 {
|
||||
chunk
|
||||
.light
|
||||
.block_light
|
||||
.get(section_idx)
|
||||
.map_or(0, |c| c.get(lx, ly, lz))
|
||||
}
|
||||
#[inline]
|
||||
fn set_light_proto(
|
||||
chunk: &mut ProtoChunk,
|
||||
section_idx: usize,
|
||||
lx: usize,
|
||||
ly: usize,
|
||||
lz: usize,
|
||||
level: u8,
|
||||
) {
|
||||
if let Some(c) = chunk.light.block_light.get_mut(section_idx) {
|
||||
c.set(lx, ly, lz, level);
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
fn propagate_level(current_level: u8, opacity: u8, _dir: BlockDirection) -> u8 {
|
||||
current_level.saturating_sub(opacity.max(1))
|
||||
}
|
||||
@ -39,12 +78,42 @@ impl LightProvider for BlockLightProvider {
|
||||
|
||||
pub struct SkyLightProvider;
|
||||
impl LightProvider for SkyLightProvider {
|
||||
#[inline]
|
||||
fn get_light(cache: &Cache, pos: BlockPos) -> u8 {
|
||||
get_sky_light(cache, pos)
|
||||
}
|
||||
#[inline]
|
||||
fn set_light(cache: &mut Cache, pos: BlockPos, level: u8) {
|
||||
set_sky_light(cache, pos, level);
|
||||
}
|
||||
#[inline]
|
||||
fn get_light_proto(
|
||||
chunk: &ProtoChunk,
|
||||
section_idx: usize,
|
||||
lx: usize,
|
||||
ly: usize,
|
||||
lz: usize,
|
||||
) -> u8 {
|
||||
chunk
|
||||
.light
|
||||
.sky_light
|
||||
.get(section_idx)
|
||||
.map_or(15, |c| c.get(lx, ly, lz))
|
||||
}
|
||||
#[inline]
|
||||
fn set_light_proto(
|
||||
chunk: &mut ProtoChunk,
|
||||
section_idx: usize,
|
||||
lx: usize,
|
||||
ly: usize,
|
||||
lz: usize,
|
||||
level: u8,
|
||||
) {
|
||||
if let Some(c) = chunk.light.sky_light.get_mut(section_idx) {
|
||||
c.set(lx, ly, lz, level);
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
fn propagate_level(current_level: u8, opacity: u8, dir: BlockDirection) -> u8 {
|
||||
if current_level == 15 && dir == BlockDirection::Down && opacity == 0 {
|
||||
return 15;
|
||||
@ -57,12 +126,114 @@ impl LightProvider for SkyLightProvider {
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PropagationEntry {
|
||||
pos: BlockPos,
|
||||
skip_direction: Option<BlockDirection>, // direction from which the light came, used to prevent back-propagation
|
||||
skip_direction: Option<BlockDirection>,
|
||||
}
|
||||
|
||||
pub struct VisitedBitSet {
|
||||
bits: Vec<u64>,
|
||||
min_x: i32,
|
||||
min_y: i32,
|
||||
min_z: i32,
|
||||
size_x: usize,
|
||||
size_y: usize,
|
||||
size_z: usize,
|
||||
}
|
||||
|
||||
impl Default for VisitedBitSet {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl VisitedBitSet {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
bits: Vec::new(),
|
||||
min_x: 0,
|
||||
min_y: 0,
|
||||
min_z: 0,
|
||||
size_x: 0,
|
||||
size_y: 0,
|
||||
size_z: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_capacity(
|
||||
&mut self,
|
||||
min_x: i32,
|
||||
min_y: i32,
|
||||
min_z: i32,
|
||||
size_x: usize,
|
||||
size_y: usize,
|
||||
size_z: usize,
|
||||
) {
|
||||
self.min_x = min_x;
|
||||
self.min_y = min_y;
|
||||
self.min_z = min_z;
|
||||
self.size_x = size_x;
|
||||
self.size_y = size_y;
|
||||
self.size_z = size_z;
|
||||
let total = size_x * size_y * size_z;
|
||||
let words = total.div_ceil(64);
|
||||
if self.bits.len() == words {
|
||||
self.bits.fill(0);
|
||||
} else {
|
||||
self.bits.resize(words, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn clear(&mut self) {
|
||||
self.bits.fill(0);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn test_and_set(&mut self, x: i32, y: i32, z: i32) -> bool {
|
||||
let lx = (x - self.min_x) as usize;
|
||||
let ly = (y - self.min_y) as usize;
|
||||
let lz = (z - self.min_z) as usize;
|
||||
if lx >= self.size_x || ly >= self.size_y || lz >= self.size_z {
|
||||
return false;
|
||||
}
|
||||
let idx = (ly * self.size_z + lz) * self.size_x + lx;
|
||||
let word = idx >> 6;
|
||||
let mask = 1u64 << (idx & 63);
|
||||
if let Some(w) = self.bits.get_mut(word) {
|
||||
let prev = *w;
|
||||
if prev & mask != 0 {
|
||||
return false;
|
||||
}
|
||||
*w = prev | mask;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn is_visited(&self, x: i32, y: i32, z: i32) -> bool {
|
||||
let lx = (x - self.min_x) as usize;
|
||||
let ly = (y - self.min_y) as usize;
|
||||
let lz = (z - self.min_z) as usize;
|
||||
if lx >= self.size_x || ly >= self.size_y || lz >= self.size_z {
|
||||
return true;
|
||||
}
|
||||
let idx = (ly * self.size_z + lz) * self.size_x + lx;
|
||||
let word = idx >> 6;
|
||||
let mask = 1u64 << (idx & 63);
|
||||
if let Some(&w) = self.bits.get(word) {
|
||||
(w & mask) != 0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LightPropagator<P: LightProvider> {
|
||||
pub(crate) queue: VecDeque<PropagationEntry>,
|
||||
pub(crate) visited: FastHashSet<BlockPos>,
|
||||
pub(crate) visited: VisitedBitSet,
|
||||
pub(crate) decrease_queue: VecDeque<(BlockPos, u8)>,
|
||||
_marker: std::marker::PhantomData<P>,
|
||||
}
|
||||
@ -71,8 +242,8 @@ impl<P: LightProvider> LightPropagator<P> {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
queue: VecDeque::with_capacity(4096),
|
||||
visited: FastHashSet::default(),
|
||||
queue: VecDeque::with_capacity(8192),
|
||||
visited: VisitedBitSet::new(),
|
||||
decrease_queue: VecDeque::new(),
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
@ -84,19 +255,21 @@ impl<P: LightProvider> LightPropagator<P> {
|
||||
self.decrease_queue.clear();
|
||||
}
|
||||
|
||||
/// Core Propagation Logic (BFS).
|
||||
///
|
||||
/// Reads and writes light directly through the light storage (a fast array
|
||||
/// lookup) instead of maintaining a separate hashed shadow cache and batched
|
||||
/// write buffer; the storage is the single source of truth.
|
||||
pub fn propagate(&mut self, cache: &mut Cache) {
|
||||
// Cache metadata for bounds checking
|
||||
let cache_x = cache.x;
|
||||
let cache_z = cache.z;
|
||||
let cache_size = cache.size;
|
||||
let min_y = cache.bottom_y() as i32;
|
||||
let max_y = min_y + cache.height() as i32;
|
||||
|
||||
let min_x = cache_x * 16;
|
||||
let min_z = cache_z * 16;
|
||||
let size_x = (cache_size * 16) as usize;
|
||||
let size_z = (cache_size * 16) as usize;
|
||||
let size_y = (max_y - min_y) as usize;
|
||||
self.visited
|
||||
.ensure_capacity(min_x, min_y, min_z, size_x, size_y, size_z);
|
||||
|
||||
while let Some(entry) = self.queue.pop_front() {
|
||||
let pos = entry.pos;
|
||||
|
||||
@ -106,7 +279,6 @@ impl<P: LightProvider> LightPropagator<P> {
|
||||
}
|
||||
|
||||
for dir in BlockDirection::all() {
|
||||
// Skip the direction we came from (if specified)
|
||||
if let Some(skip_dir) = entry.skip_direction
|
||||
&& dir == skip_dir
|
||||
{
|
||||
@ -114,36 +286,78 @@ impl<P: LightProvider> LightPropagator<P> {
|
||||
}
|
||||
|
||||
let neighbor_pos = pos.offset(dir.to_offset());
|
||||
let nx = neighbor_pos.0.x;
|
||||
let ny = neighbor_pos.0.y;
|
||||
let nz = neighbor_pos.0.z;
|
||||
|
||||
// Skip if already visited (critical early-exit optimization)
|
||||
if self.visited.contains(&neighbor_pos) {
|
||||
if self.visited.is_visited(nx, ny, nz) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip neighbor if it's outside world bounds
|
||||
if neighbor_pos.0.y < min_y || neighbor_pos.0.y >= max_y {
|
||||
if ny < min_y || ny >= max_y {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (cx, _rel) = neighbor_pos.chunk_and_chunk_relative_position();
|
||||
let rel_x = cx.x - cache_x;
|
||||
let rel_z = cx.y - cache_z;
|
||||
let rel_x = (nx >> 4) - cache_x;
|
||||
let rel_z = (nz >> 4) - cache_z;
|
||||
if rel_x < 0 || rel_x >= cache_size || rel_z < 0 || rel_z >= cache_size {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get block opacity
|
||||
let state = cache.get_block_state(&neighbor_pos.0);
|
||||
let opacity = state.to_state().opacity;
|
||||
let chunk_idx = (rel_x * cache_size + rel_z) as usize;
|
||||
let local_x = (nx & 15) as usize;
|
||||
let local_z = (nz & 15) as usize;
|
||||
|
||||
let section_idx = ((ny - min_y) >> 4) as usize;
|
||||
let local_y = (ny & 15) as usize;
|
||||
|
||||
let (opacity, neighbor_light) = match &cache.chunks[chunk_idx] {
|
||||
Chunk::Proto(c) => {
|
||||
let local_y_proto = ny - min_y;
|
||||
let state_id =
|
||||
c.get_block_state_raw(local_x as i32, local_y_proto, local_z as i32);
|
||||
let op = if state_id == BlockStateId::AIR {
|
||||
0
|
||||
} else {
|
||||
state_id.to_state().opacity
|
||||
};
|
||||
let light = P::get_light_proto(c, section_idx, local_x, local_y, local_z);
|
||||
(op, light)
|
||||
}
|
||||
Chunk::Level(lvl) => {
|
||||
let state_id = lvl
|
||||
.section
|
||||
.get_block_absolute_y(local_x, ny, local_z)
|
||||
.unwrap_or(BlockStateId::AIR);
|
||||
let op = if state_id == BlockStateId::AIR {
|
||||
0
|
||||
} else {
|
||||
state_id.to_state().opacity
|
||||
};
|
||||
(op, P::get_light(cache, neighbor_pos))
|
||||
}
|
||||
};
|
||||
|
||||
let new_level = P::propagate_level(current_light, opacity, dir);
|
||||
let neighbor_light = P::get_light(cache, neighbor_pos);
|
||||
|
||||
if new_level > neighbor_light {
|
||||
match &mut cache.chunks[chunk_idx] {
|
||||
Chunk::Proto(c) => {
|
||||
P::set_light_proto(
|
||||
c,
|
||||
section_idx,
|
||||
local_x,
|
||||
local_y,
|
||||
local_z,
|
||||
new_level,
|
||||
);
|
||||
}
|
||||
Chunk::Level(_) => {
|
||||
P::set_light(cache, neighbor_pos, new_level);
|
||||
}
|
||||
}
|
||||
|
||||
// Add to propagation queue if bright enough
|
||||
if new_level > 1 && self.visited.insert(neighbor_pos) {
|
||||
if new_level > 1 && self.visited.test_and_set(nx, ny, nz) {
|
||||
self.queue.push_back(PropagationEntry {
|
||||
pos: neighbor_pos,
|
||||
skip_direction: Some(dir.opposite()),
|
||||
@ -154,10 +368,7 @@ impl<P: LightProvider> LightPropagator<P> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle light removal
|
||||
pub fn process_decrease_queue(&mut self, cache: &mut Cache) {
|
||||
{
|
||||
// Cache metadata for bounds checking
|
||||
let cache_x = cache.x;
|
||||
let cache_z = cache.z;
|
||||
let cache_size = cache.size;
|
||||
@ -166,7 +377,6 @@ impl<P: LightProvider> LightPropagator<P> {
|
||||
for dir in BlockDirection::all() {
|
||||
let neighbor_pos = pos.offset(dir.to_offset());
|
||||
|
||||
// Bounds check
|
||||
let (cx, _rel) = neighbor_pos.chunk_and_chunk_relative_position();
|
||||
let rel_x = cx.x - cache_x;
|
||||
let rel_z = cx.y - cache_z;
|
||||
@ -186,23 +396,23 @@ impl<P: LightProvider> LightPropagator<P> {
|
||||
let predicted = P::propagate_level(old_val, opacity, dir);
|
||||
|
||||
if neighbor_light == predicted || neighbor_light < old_val {
|
||||
// Darken
|
||||
P::set_light(cache, neighbor_pos, 0);
|
||||
self.decrease_queue
|
||||
.push_back((neighbor_pos, neighbor_light));
|
||||
} else if neighbor_light >= old_val {
|
||||
// Re-illuminate from this bright neighbor
|
||||
let nx = neighbor_pos.0.x;
|
||||
let ny = neighbor_pos.0.y;
|
||||
let nz = neighbor_pos.0.z;
|
||||
self.queue.push_back(PropagationEntry {
|
||||
pos: neighbor_pos,
|
||||
skip_direction: None,
|
||||
});
|
||||
self.visited.insert(neighbor_pos);
|
||||
}
|
||||
self.visited.test_and_set(nx, ny, nz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.propagate(cache); // Re-propagate from survivors
|
||||
self.propagate(cache);
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,8 +429,6 @@ impl BlockLightPropagator {
|
||||
pub fn propagate_light(&mut self, cache: &mut Cache) {
|
||||
self.clear();
|
||||
|
||||
//let scan_start = Instant::now();
|
||||
|
||||
let min_y = cache.bottom_y() as i32;
|
||||
let max_y = min_y + cache.height() as i32;
|
||||
let center_x = cache.x + (cache.size / 2);
|
||||
@ -231,17 +439,79 @@ impl BlockLightPropagator {
|
||||
let end_x = start_x + 18;
|
||||
let end_z = start_z + 18;
|
||||
|
||||
for y in min_y..max_y {
|
||||
let min_x = cache.x * 16;
|
||||
let min_z = cache.z * 16;
|
||||
let size_x = (cache.size * 16) as usize;
|
||||
let size_z = (cache.size * 16) as usize;
|
||||
let size_y = (max_y - min_y) as usize;
|
||||
self.visited
|
||||
.ensure_capacity(min_x, min_y, min_z, size_x, size_y, size_z);
|
||||
|
||||
for z in start_z..end_z {
|
||||
let rel_z = (z >> 4) - cache.z;
|
||||
let local_z = (z & 15) as usize;
|
||||
|
||||
for x in start_x..end_x {
|
||||
let pos_vec = Vector3::new(x, y, z);
|
||||
let state = cache.get_block_state(&pos_vec);
|
||||
let emission = state.to_state().luminance;
|
||||
let rel_x = (x >> 4) - cache.x;
|
||||
if rel_x < 0 || rel_x >= cache.size || rel_z < 0 || rel_z >= cache.size {
|
||||
continue;
|
||||
}
|
||||
let chunk_idx = (rel_x * cache.size + rel_z) as usize;
|
||||
let local_x = (x & 15) as usize;
|
||||
|
||||
match &mut cache.chunks[chunk_idx] {
|
||||
Chunk::Proto(c) => {
|
||||
for y in min_y..max_y {
|
||||
let local_y_proto = y - min_y;
|
||||
let state_id = c.get_block_state_raw(
|
||||
local_x as i32,
|
||||
local_y_proto,
|
||||
local_z as i32,
|
||||
);
|
||||
if state_id == BlockStateId::AIR {
|
||||
continue;
|
||||
}
|
||||
let emission = state_id.to_state().luminance;
|
||||
if emission > 0 {
|
||||
let pos = BlockPos(pos_vec);
|
||||
set_block_light(cache, pos, emission);
|
||||
if self.visited.insert(pos) {
|
||||
// Block light propagates in all directions
|
||||
let section_idx = (local_y_proto >> 4) as usize;
|
||||
let local_y = (y & 15) as usize;
|
||||
if section_idx < c.light.block_light.len() {
|
||||
c.light.block_light[section_idx]
|
||||
.set(local_x, local_y, local_z, emission);
|
||||
}
|
||||
if self.visited.test_and_set(x, y, z) {
|
||||
let pos = BlockPos(Vector3::new(x, y, z));
|
||||
self.queue.push_back(PropagationEntry {
|
||||
pos,
|
||||
skip_direction: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Chunk::Level(lvl) => {
|
||||
let mut light_engine = lvl
|
||||
.light_engine
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
for y in min_y..max_y {
|
||||
let state_id = lvl
|
||||
.section
|
||||
.get_block_absolute_y(local_x, y, local_z)
|
||||
.unwrap_or(BlockStateId::AIR);
|
||||
if state_id == BlockStateId::AIR {
|
||||
continue;
|
||||
}
|
||||
let emission = state_id.to_state().luminance;
|
||||
if emission > 0 {
|
||||
let section_idx = ((y - min_y) >> 4) as usize;
|
||||
let local_y = (y & 15) as usize;
|
||||
if section_idx < light_engine.block_light.len() {
|
||||
light_engine.block_light[section_idx]
|
||||
.set(local_x, local_y, local_z, emission);
|
||||
}
|
||||
if self.visited.test_and_set(x, y, z) {
|
||||
let pos = BlockPos(Vector3::new(x, y, z));
|
||||
self.queue.push_back(PropagationEntry {
|
||||
pos,
|
||||
skip_direction: None,
|
||||
@ -251,13 +521,10 @@ impl BlockLightPropagator {
|
||||
}
|
||||
}
|
||||
}
|
||||
//let scan_elapsed = scan_start.elapsed();
|
||||
//let propagate_start = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
self.propagate(cache);
|
||||
|
||||
//let propagate_elapsed = propagate_start.elapsed();
|
||||
//log::info!("Block light timing - Scan: {:?}, Propagate: {:?}", scan_elapsed, propagate_elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,8 +533,6 @@ impl SkyLightPropagator {
|
||||
pub fn convert_light(&mut self, cache: &mut Cache) {
|
||||
self.clear();
|
||||
|
||||
//let scan_start = Instant::now();
|
||||
|
||||
let center_x = cache.x + (cache.size / 2);
|
||||
let center_z = cache.z + (cache.size / 2);
|
||||
let start_x = center_x * 16 - 1;
|
||||
@ -278,25 +543,29 @@ impl SkyLightPropagator {
|
||||
let bottom_y = cache.bottom_y() as i32;
|
||||
let max_y = bottom_y + cache.height() as i32;
|
||||
|
||||
// Pre-allocate with exact size needed
|
||||
let capacity = ((end_x - start_x) * (end_z - start_z)) as usize;
|
||||
let mut surface_heights =
|
||||
FastHashMap::with_capacity_and_hasher(capacity, rustc_hash::FxBuildHasher);
|
||||
let min_x = cache.x * 16;
|
||||
let min_z = cache.z * 16;
|
||||
let size_x = (cache.size * 16) as usize;
|
||||
let size_z = (cache.size * 16) as usize;
|
||||
let size_y = (max_y - bottom_y) as usize;
|
||||
self.visited
|
||||
.ensure_capacity(min_x, bottom_y, min_z, size_x, size_y, size_z);
|
||||
|
||||
let mut surface_heights = [0i32; 18 * 18];
|
||||
|
||||
// Process in Z-outer, X-inner order for better cache locality
|
||||
for z in start_z..end_z {
|
||||
let chunk_z = z >> 4;
|
||||
let local_z = (z & 15) as usize;
|
||||
let lz = (z - start_z) as usize;
|
||||
|
||||
for x in start_x..end_x {
|
||||
let chunk_x = x >> 4;
|
||||
let local_x = (x & 15) as usize;
|
||||
let lx = (x - start_x) as usize;
|
||||
|
||||
// Get heightmap (top solid blocks)
|
||||
let top_y = cache.get_top_y(&HeightMap::WorldSurface, x, z);
|
||||
surface_heights.insert((x, z), top_y);
|
||||
surface_heights[lx * 18 + lz] = top_y;
|
||||
|
||||
// Get chunk index once per column
|
||||
let rel_x = chunk_x - cache.x;
|
||||
let rel_z = chunk_z - cache.z;
|
||||
|
||||
@ -306,16 +575,47 @@ impl SkyLightPropagator {
|
||||
|
||||
let chunk_idx = (rel_x * cache.size + rel_z) as usize;
|
||||
|
||||
// Fill everything above heightmap with 15 immediately
|
||||
for y in (top_y + 1)..max_y {
|
||||
let section_idx = ((y - bottom_y) >> 4) as usize;
|
||||
let local_y = (y & 15) as usize;
|
||||
|
||||
// Direct array access - skip all function call overhead
|
||||
match &mut cache.chunks[chunk_idx] {
|
||||
Chunk::Proto(c) => {
|
||||
let top_local_y = (top_y + 1 - bottom_y).max(0) as usize;
|
||||
let top_sec = top_local_y >> 4;
|
||||
let top_rem = top_local_y & 15;
|
||||
if top_sec < c.light.sky_light.len() {
|
||||
c.light.sky_light[top_sec]
|
||||
.set_column_y_range(local_x, local_z, top_rem, 16, 15);
|
||||
for sec in (top_sec + 1)..c.light.sky_light.len() {
|
||||
c.light.sky_light[sec]
|
||||
.set_column_y_range(local_x, local_z, 0, 16, 15);
|
||||
}
|
||||
}
|
||||
|
||||
let mut light: i32 = 15;
|
||||
for y in (bottom_y..=top_y).rev() {
|
||||
let local_y_proto = y - bottom_y;
|
||||
let state_id = c.get_block_state_raw(
|
||||
local_x as i32,
|
||||
local_y_proto,
|
||||
local_z as i32,
|
||||
);
|
||||
let opacity = if state_id == BlockStateId::AIR {
|
||||
0
|
||||
} else {
|
||||
state_id.to_state().opacity as i32
|
||||
};
|
||||
|
||||
light = light.saturating_sub(opacity);
|
||||
let light_val = if light <= 0 { 0 } else { light as u8 };
|
||||
let section_idx = (local_y_proto >> 4) as usize;
|
||||
let local_y = (y & 15) as usize;
|
||||
|
||||
if section_idx < c.light.sky_light.len() {
|
||||
c.light.sky_light[section_idx].set(local_x, local_y, local_z, 15);
|
||||
c.light.sky_light[section_idx]
|
||||
.set(local_x, local_y, local_z, light_val);
|
||||
}
|
||||
|
||||
if light <= 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Chunk::Level(c) => {
|
||||
@ -323,72 +623,75 @@ impl SkyLightPropagator {
|
||||
.light_engine
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
|
||||
for y in (top_y + 1)..max_y {
|
||||
let section_idx = ((y - bottom_y) >> 4) as usize;
|
||||
let local_y = (y & 15) as usize;
|
||||
if section_idx < light_engine.sky_light.len() {
|
||||
light_engine.sky_light[section_idx]
|
||||
.set(local_x, local_y, local_z, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only iterate from top_y DOWN - not from max_y
|
||||
let mut light: i32 = 15;
|
||||
|
||||
for y in (bottom_y..=top_y).rev() {
|
||||
let section_idx = ((y - bottom_y) >> 4) as usize;
|
||||
let local_y = (y & 15) as usize;
|
||||
|
||||
// Get block opacity
|
||||
let opacity = {
|
||||
let pos_vec = Vector3::new(x, y, z);
|
||||
let state = cache.get_block_state(&pos_vec);
|
||||
state.to_state().opacity
|
||||
} as i32;
|
||||
let state_id = c
|
||||
.section
|
||||
.get_block_absolute_y(local_x, y, local_z)
|
||||
.unwrap_or(BlockStateId::AIR);
|
||||
let opacity = if state_id == BlockStateId::AIR {
|
||||
0
|
||||
} else {
|
||||
state_id.to_state().opacity as i32
|
||||
};
|
||||
|
||||
// Reduce light by opacity
|
||||
light = light.saturating_sub(opacity);
|
||||
|
||||
// Set the light value directly
|
||||
let light_val = if light <= 0 { 0 } else { light as u8 };
|
||||
|
||||
match &mut cache.chunks[chunk_idx] {
|
||||
Chunk::Proto(c) => {
|
||||
if section_idx < c.light.sky_light.len() {
|
||||
c.light.sky_light[section_idx]
|
||||
.set(local_x, local_y, local_z, light_val);
|
||||
}
|
||||
}
|
||||
Chunk::Level(c) => {
|
||||
let mut light_engine = c
|
||||
.light_engine
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if section_idx < light_engine.sky_light.len() {
|
||||
light_engine.sky_light[section_idx]
|
||||
.set(local_x, local_y, local_z, light_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Early exit when light hits 0
|
||||
if light <= 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue horizontal propagation
|
||||
for z in start_z..end_z {
|
||||
let lz = (z - start_z) as usize;
|
||||
for x in start_x..end_x {
|
||||
let top_y = surface_heights[&(x, z)];
|
||||
let lx = (x - start_x) as usize;
|
||||
let top_y = surface_heights[lx * 18 + lz];
|
||||
|
||||
let north_top = surface_heights.get(&(x, z - 1)).copied().unwrap_or(top_y);
|
||||
let south_top = surface_heights.get(&(x, z + 1)).copied().unwrap_or(top_y);
|
||||
let west_top = surface_heights.get(&(x - 1, z)).copied().unwrap_or(top_y);
|
||||
let east_top = surface_heights.get(&(x + 1, z)).copied().unwrap_or(top_y);
|
||||
let north_top = if lz > 0 {
|
||||
surface_heights[lx * 18 + (lz - 1)]
|
||||
} else {
|
||||
top_y
|
||||
};
|
||||
let south_top = if lz + 1 < 18 {
|
||||
surface_heights[lx * 18 + (lz + 1)]
|
||||
} else {
|
||||
top_y
|
||||
};
|
||||
let west_top = if lx > 0 {
|
||||
surface_heights[(lx - 1) * 18 + lz]
|
||||
} else {
|
||||
top_y
|
||||
};
|
||||
let east_top = if lx + 1 < 18 {
|
||||
surface_heights[(lx + 1) * 18 + lz]
|
||||
} else {
|
||||
top_y
|
||||
};
|
||||
|
||||
// We must check up to the highest neighbor to catch the "air sources"
|
||||
let max_check_y = top_y
|
||||
.max(north_top)
|
||||
.max(south_top)
|
||||
@ -399,7 +702,6 @@ impl SkyLightPropagator {
|
||||
let pos = BlockPos(Vector3::new(x, y, z));
|
||||
let light = get_sky_light(cache, pos);
|
||||
|
||||
// Use continue, or only break if we are safely below all possible side-light
|
||||
if light == 0 {
|
||||
if y <= top_y {
|
||||
break;
|
||||
@ -411,7 +713,7 @@ impl SkyLightPropagator {
|
||||
let below_neighbor =
|
||||
y < north_top || y < south_top || y < west_top || y < east_top;
|
||||
|
||||
if (is_at_surface || below_neighbor) && self.visited.insert(pos) {
|
||||
if (is_at_surface || below_neighbor) && self.visited.test_and_set(x, y, z) {
|
||||
let skip_dir = (y >= top_y).then_some(BlockDirection::Up);
|
||||
|
||||
self.queue.push_back(PropagationEntry {
|
||||
@ -423,13 +725,7 @@ impl SkyLightPropagator {
|
||||
}
|
||||
}
|
||||
|
||||
//let propagate_start = Instant::now();
|
||||
|
||||
self.propagate(cache);
|
||||
|
||||
//let propagate_elapsed = propagate_start.elapsed();
|
||||
//let scan_elapsed = scan_start.elapsed();
|
||||
//log::info!("Sky light timing - Scan: {:?}, Propagate: {:?}", scan_elapsed, propagate_elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ -474,7 +770,6 @@ impl LightEngine {
|
||||
old_luminance: u8,
|
||||
new_luminance: u8,
|
||||
) {
|
||||
// Decrease Logic
|
||||
if old_luminance > new_luminance {
|
||||
let current_light = get_block_light(cache, pos);
|
||||
if current_light > 0 {
|
||||
@ -485,10 +780,13 @@ impl LightEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// Increase Logic
|
||||
if new_luminance > 0 {
|
||||
set_block_light(cache, pos, new_luminance);
|
||||
if self.block_light.visited.insert(pos) {
|
||||
if self
|
||||
.block_light
|
||||
.visited
|
||||
.test_and_set(pos.0.x, pos.0.y, pos.0.z)
|
||||
{
|
||||
self.block_light.queue.push_back(PropagationEntry {
|
||||
pos,
|
||||
skip_direction: None,
|
||||
|
||||
@ -3,6 +3,7 @@ use crate::chunk_system::generation_cache::Cache;
|
||||
use crate::generation::height_limit::HeightLimitView;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
|
||||
#[inline]
|
||||
const fn get_chunk_index(cache: &Cache, chunk_x: i32, chunk_z: i32) -> Option<usize> {
|
||||
let rel_x = chunk_x - cache.x;
|
||||
let rel_z = chunk_z - cache.z;
|
||||
@ -12,6 +13,7 @@ const fn get_chunk_index(cache: &Cache, chunk_x: i32, chunk_z: i32) -> Option<us
|
||||
Some((rel_x * cache.size + rel_z) as usize)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_section_y(cache: &Cache, pos_y: i32) -> Option<usize> {
|
||||
let bottom = cache.bottom_y() as i32;
|
||||
if pos_y < bottom {
|
||||
@ -21,6 +23,7 @@ fn get_section_y(cache: &Cache, pos_y: i32) -> Option<usize> {
|
||||
Some(section)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn get_block_light(cache: &Cache, pos: BlockPos) -> u8 {
|
||||
let chunk_x = pos.0.x >> 4;
|
||||
@ -58,6 +61,7 @@ pub fn get_block_light(cache: &Cache, pos: BlockPos) -> u8 {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_block_light(cache: &mut Cache, pos: BlockPos, level: u8) {
|
||||
let chunk_x = pos.0.x >> 4;
|
||||
let chunk_z = pos.0.z >> 4;
|
||||
@ -99,6 +103,7 @@ pub fn set_block_light(cache: &mut Cache, pos: BlockPos, level: u8) {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn get_sky_light(cache: &Cache, pos: BlockPos) -> u8 {
|
||||
let chunk_x = pos.0.x >> 4;
|
||||
@ -136,6 +141,7 @@ pub fn get_sky_light(cache: &Cache, pos: BlockPos) -> u8 {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_sky_light(cache: &mut Cache, pos: BlockPos, level: u8) {
|
||||
let chunk_x = pos.0.x >> 4;
|
||||
let chunk_z = pos.0.z >> 4;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::{info, warn};
|
||||
@ -70,16 +70,16 @@ pub struct PoiSectionData {
|
||||
pub struct PoiChunkData {
|
||||
pub data_version: i32,
|
||||
/// Sections keyed by Y section coordinate (e.g., "-1", "0", "1", "4")
|
||||
pub sections: HashMap<String, PoiSectionData>,
|
||||
pub sections: FxHashMap<String, PoiSectionData>,
|
||||
}
|
||||
|
||||
/// POI data for a single region (32x32 chunks) using MCA format
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PoiRegion {
|
||||
/// Entries indexed by position
|
||||
entries: HashMap<(i32, i32, i32), PoiEntry>,
|
||||
entries: FxHashMap<(i32, i32, i32), PoiEntry>,
|
||||
/// Track which chunks are dirty
|
||||
dirty_chunks: std::collections::HashSet<(i32, i32)>,
|
||||
dirty_chunks: rustc_hash::FxHashSet<(i32, i32)>,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ impl PoiRegion {
|
||||
|
||||
/// Group entries by chunk, then create chunk NBT data
|
||||
fn get_chunk_data(&self, chunk_x: i32, chunk_z: i32) -> Option<PoiChunkData> {
|
||||
let mut sections: HashMap<String, PoiSectionData> = HashMap::new();
|
||||
let mut sections: FxHashMap<String, PoiSectionData> = FxHashMap::default();
|
||||
|
||||
for entry in self.entries.values() {
|
||||
let entry_chunk_x = entry.x >> 4;
|
||||
@ -218,7 +218,7 @@ impl PoiRegion {
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
|
||||
|
||||
let data_version = nbt.get_int("DataVersion").unwrap_or(DATA_VERSION);
|
||||
let mut sections = HashMap::new();
|
||||
let mut sections = FxHashMap::default();
|
||||
|
||||
if let Some(sec_tag) = nbt.get_compound("Sections") {
|
||||
for (sec_key, tag) in &sec_tag.child_tags {
|
||||
@ -269,11 +269,11 @@ impl PoiRegion {
|
||||
}
|
||||
|
||||
// Build all chunk data
|
||||
let mut chunk_data_map: HashMap<usize, Vec<u8>> = HashMap::new();
|
||||
let mut chunk_data_map: FxHashMap<usize, Vec<u8>> = FxHashMap::default();
|
||||
|
||||
// Collect all unique chunks that have entries
|
||||
let mut chunks_with_data: std::collections::HashSet<(i32, i32)> =
|
||||
std::collections::HashSet::new();
|
||||
let mut chunks_with_data: rustc_hash::FxHashSet<(i32, i32)> =
|
||||
rustc_hash::FxHashSet::default();
|
||||
for entry in self.entries.values() {
|
||||
chunks_with_data.insert((entry.x >> 4, entry.z >> 4));
|
||||
}
|
||||
@ -429,7 +429,7 @@ pub struct PoiStorage {
|
||||
/// Path to the poi folder
|
||||
folder: PathBuf,
|
||||
/// Loaded regions, keyed by (`region_x`, `region_z`)
|
||||
regions: HashMap<(i32, i32), PoiRegion>,
|
||||
regions: FxHashMap<(i32, i32), PoiRegion>,
|
||||
}
|
||||
|
||||
impl PoiStorage {
|
||||
@ -437,7 +437,7 @@ impl PoiStorage {
|
||||
pub fn new(poi_folder: PathBuf) -> Self {
|
||||
Self {
|
||||
folder: poi_folder,
|
||||
regions: HashMap::new(),
|
||||
regions: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ wasmparser.workspace = true
|
||||
wasm-encoder = "0.257"
|
||||
|
||||
# authentication
|
||||
ureq = { workspace = true, features = ["json"] }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
|
||||
sha1.workspace = true
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::block::{
|
||||
BlockBehaviour, BlockIsReplacing, BlockMetadata, CanPlaceAtArgs, CanUpdateAtArgs,
|
||||
@ -154,7 +154,7 @@ fn get_attach_direction(
|
||||
replacing_block,
|
||||
))
|
||||
} else {
|
||||
HashSet::new()
|
||||
FxHashSet::default()
|
||||
};
|
||||
|
||||
if let Some(player) = player_wrapper {
|
||||
@ -183,8 +183,8 @@ const fn is_solid_face(block: &Block) -> bool {
|
||||
block.default_state.is_full_cube()
|
||||
}
|
||||
|
||||
fn active_directions(props: GlowLichenLikeProperties) -> HashSet<BlockDirection> {
|
||||
let mut set = HashSet::new();
|
||||
fn active_directions(props: GlowLichenLikeProperties) -> FxHashSet<BlockDirection> {
|
||||
let mut set = FxHashSet::default();
|
||||
if props.down {
|
||||
set.insert(BlockDirection::Down);
|
||||
}
|
||||
|
||||
@ -1,17 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use pumpkin_data::block_properties::{
|
||||
BlockProperties, TrialSpawnerLikeProperties, TrialSpawnerState,
|
||||
};
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||
use pumpkin_macros::pumpkin_block;
|
||||
use pumpkin_world::world::BlockFlags;
|
||||
|
||||
use crate::block::entities::trial_spawner::TrialSpawnerBlockEntity;
|
||||
use crate::block::registry::BlockActionResult;
|
||||
use crate::block::{BlockBehaviour, NormalUseArgs, PlacedArgs, UseWithItemArgs};
|
||||
use crate::block::{BlockBehaviour, PlacedArgs};
|
||||
|
||||
#[pumpkin_block("minecraft:trial_spawner")]
|
||||
pub struct TrialSpawnerBlock;
|
||||
@ -21,95 +13,4 @@ impl BlockBehaviour for TrialSpawnerBlock {
|
||||
let entity = TrialSpawnerBlockEntity::new(*args.position);
|
||||
args.world.add_block_entity(Arc::new(entity));
|
||||
}
|
||||
|
||||
fn normal_use(&self, args: NormalUseArgs<'_>) -> BlockActionResult {
|
||||
let state_id = args.world.get_block_state_id(args.position);
|
||||
let mut props = TrialSpawnerLikeProperties::from_state_id(state_id, args.block);
|
||||
|
||||
match props.trial_spawner_state {
|
||||
TrialSpawnerState::Inactive | TrialSpawnerState::WaitingForPlayers => {
|
||||
props.trial_spawner_state = TrialSpawnerState::Active;
|
||||
args.world.set_block_state(
|
||||
args.position,
|
||||
props.to_state_id(args.block),
|
||||
BlockFlags::NOTIFY_ALL,
|
||||
);
|
||||
|
||||
args.world.play_sound(
|
||||
Sound::BlockTrialSpawnerDetectPlayer,
|
||||
SoundCategory::Blocks,
|
||||
&args.position.to_f64(),
|
||||
);
|
||||
args.world.play_sound(
|
||||
Sound::BlockTrialSpawnerOpenShutter,
|
||||
SoundCategory::Blocks,
|
||||
&args.position.to_f64(),
|
||||
);
|
||||
}
|
||||
TrialSpawnerState::Active => {
|
||||
// Eject trial rewards & key drop upon trial wave completion
|
||||
props.trial_spawner_state = TrialSpawnerState::WaitingForRewardEjection;
|
||||
args.world.set_block_state(
|
||||
args.position,
|
||||
props.to_state_id(args.block),
|
||||
BlockFlags::NOTIFY_ALL,
|
||||
);
|
||||
|
||||
args.world.play_sound(
|
||||
Sound::BlockTrialSpawnerSpawnItemBegin,
|
||||
SoundCategory::Blocks,
|
||||
&args.position.to_f64(),
|
||||
);
|
||||
args.world.play_sound(
|
||||
Sound::BlockTrialSpawnerEjectItem,
|
||||
SoundCategory::Blocks,
|
||||
&args.position.to_f64(),
|
||||
);
|
||||
|
||||
let key_stack = ItemStack::new(1, &Item::TRIAL_KEY);
|
||||
args.world.drop_stack(args.position, key_stack);
|
||||
|
||||
props.trial_spawner_state = TrialSpawnerState::Cooldown;
|
||||
args.world.set_block_state(
|
||||
args.position,
|
||||
props.to_state_id(args.block),
|
||||
BlockFlags::NOTIFY_ALL,
|
||||
);
|
||||
|
||||
args.world.play_sound(
|
||||
Sound::BlockTrialSpawnerCloseShutter,
|
||||
SoundCategory::Blocks,
|
||||
&args.position.to_f64(),
|
||||
);
|
||||
}
|
||||
TrialSpawnerState::Cooldown => {
|
||||
args.world.play_sound(
|
||||
Sound::BlockTrialSpawnerAmbient,
|
||||
SoundCategory::Blocks,
|
||||
&args.position.to_f64(),
|
||||
);
|
||||
}
|
||||
TrialSpawnerState::WaitingForRewardEjection | TrialSpawnerState::EjectingReward => {
|
||||
props.trial_spawner_state = TrialSpawnerState::Cooldown;
|
||||
args.world.set_block_state(
|
||||
args.position,
|
||||
props.to_state_id(args.block),
|
||||
BlockFlags::NOTIFY_ALL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlockActionResult::Success
|
||||
}
|
||||
|
||||
fn use_with_item(&self, args: UseWithItemArgs<'_>) -> BlockActionResult {
|
||||
self.normal_use(NormalUseArgs {
|
||||
server: args.server,
|
||||
world: args.world,
|
||||
block: args.block,
|
||||
position: args.position,
|
||||
player: args.player,
|
||||
hit: args.hit,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,11 +6,7 @@ use std::sync::{
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use pumpkin_data::{entity::EntityType, world::WorldEvent};
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_util::math::{
|
||||
boundingbox::{BoundingBox, EntityDimensions},
|
||||
position::BlockPos,
|
||||
vector3::Vector3,
|
||||
};
|
||||
use pumpkin_util::math::{position::BlockPos, vector3::Vector3};
|
||||
|
||||
use crate::{block::entities::BlockEntity, entity::EntityBase, world::World};
|
||||
|
||||
@ -161,16 +157,10 @@ impl BlockEntity for MobSpawnerBlockEntity {
|
||||
+ (rand::random::<f64>() - rand::random::<f64>()) * spawn_range as f64
|
||||
+ 0.5,
|
||||
);
|
||||
// TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes
|
||||
if !world.is_space_empty(BoundingBox::new_from_pos(
|
||||
if !world.is_space_empty(entity_type.get_spawn_bounding_box(
|
||||
spawn_pos.x,
|
||||
spawn_pos.y,
|
||||
spawn_pos.z,
|
||||
&EntityDimensions {
|
||||
width: entity_type.dimension[0],
|
||||
height: entity_type.dimension[1],
|
||||
eye_height: entity_type.eye_height,
|
||||
},
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
use super::BlockEntity;
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use std::collections::HashSet;
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -9,7 +9,7 @@ pub struct VaultBlockEntity {
|
||||
pub position: BlockPos,
|
||||
pub config: Mutex<Option<NbtCompound>>,
|
||||
pub server_data: Mutex<Option<NbtCompound>>,
|
||||
pub rewarded_players: Mutex<HashSet<Uuid>>,
|
||||
pub rewarded_players: Mutex<FxHashSet<Uuid>>,
|
||||
}
|
||||
|
||||
impl BlockEntity for VaultBlockEntity {
|
||||
@ -29,7 +29,7 @@ impl BlockEntity for VaultBlockEntity {
|
||||
position,
|
||||
config: Mutex::new(nbt.get_compound("config").cloned()),
|
||||
server_data: Mutex::new(nbt.get_compound("server_data").cloned()),
|
||||
rewarded_players: Mutex::new(HashSet::new()),
|
||||
rewarded_players: Mutex::new(FxHashSet::default()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ impl VaultBlockEntity {
|
||||
position,
|
||||
config: Mutex::new(None),
|
||||
server_data: Mutex::new(None),
|
||||
rewarded_players: Mutex::new(HashSet::new()),
|
||||
rewarded_players: Mutex::new(FxHashSet::default()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ use crate::{
|
||||
dispatcher::CommandError,
|
||||
tree::{RawArg, RawArgs},
|
||||
},
|
||||
net::authentication::lookup_profile_by_name,
|
||||
net::authentication::lookup_profile_by_name_blocking,
|
||||
net::{GameProfile, offline_uuid},
|
||||
server::Server,
|
||||
};
|
||||
@ -254,7 +254,7 @@ fn resolve_profiles_from_token(
|
||||
}
|
||||
|
||||
if server.advanced_config.networking.java.online_mode {
|
||||
match lookup_profile_by_name(
|
||||
match lookup_profile_by_name_blocking(
|
||||
raw_arg.value,
|
||||
&server.advanced_config.networking.java.authentication,
|
||||
) {
|
||||
|
||||
@ -10,7 +10,7 @@ use crate::command::errors::command_syntax_error::CommandSyntaxError;
|
||||
use crate::command::errors::error_types::CommandErrorType;
|
||||
use crate::command::string_reader::StringReader;
|
||||
use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder};
|
||||
use crate::net::authentication::lookup_profile_by_name;
|
||||
use crate::net::authentication::lookup_profile_by_name_blocking;
|
||||
use crate::net::{GameProfile, offline_uuid};
|
||||
use crate::server::Server;
|
||||
use arc_swap::ArcSwap;
|
||||
@ -72,7 +72,7 @@ impl GameProfileResult {
|
||||
}
|
||||
|
||||
if server.advanced_config.networking.java.online_mode {
|
||||
return match lookup_profile_by_name(
|
||||
return match lookup_profile_by_name_blocking(
|
||||
name,
|
||||
&server.advanced_config.networking.java.authentication,
|
||||
) {
|
||||
|
||||
@ -142,7 +142,7 @@ fn report_resolved_profile(
|
||||
source.send_feedback(msg, false);
|
||||
}
|
||||
|
||||
fn fetch_profile_by_name_helper(server: &Server, name: &str) -> Option<GameProfile> {
|
||||
async fn fetch_profile_by_name_helper(server: &Server, name: &str) -> Option<GameProfile> {
|
||||
if let Some(player) = server.get_player_by_name(name) {
|
||||
return Some(player.gameprofile.clone());
|
||||
}
|
||||
@ -155,7 +155,10 @@ fn fetch_profile_by_name_helper(server: &Server, name: &str) -> Option<GameProfi
|
||||
.java
|
||||
.authentication
|
||||
.clone();
|
||||
let mojang_res = lookup_profile_by_name(name, &auth_config).ok().flatten();
|
||||
let mojang_res = lookup_profile_by_name(name, &auth_config)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if let Some((uuid, resolved_name)) = mojang_res {
|
||||
server
|
||||
@ -171,6 +174,7 @@ fn fetch_profile_by_name_helper(server: &Server, name: &str) -> Option<GameProfi
|
||||
.authentication
|
||||
.clone();
|
||||
let full_profile = fetch_profile_by_uuid(uuid, &auth_config_clone)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
@ -212,7 +216,7 @@ fn fetch_profile_by_name_helper(server: &Server, name: &str) -> Option<GameProfi
|
||||
None
|
||||
}
|
||||
|
||||
fn fetch_profile_by_id_helper(server: &Server, id: Uuid) -> Option<GameProfile> {
|
||||
async fn fetch_profile_by_id_helper(server: &Server, id: Uuid) -> Option<GameProfile> {
|
||||
if let Some(player) = server.get_player_by_uuid(id) {
|
||||
return Some(player.gameprofile.clone());
|
||||
}
|
||||
@ -223,7 +227,7 @@ fn fetch_profile_by_id_helper(server: &Server, id: Uuid) -> Option<GameProfile>
|
||||
.java
|
||||
.authentication
|
||||
.clone();
|
||||
let mojang_res = fetch_profile_by_uuid(id, &auth_config).ok().flatten();
|
||||
let mojang_res = fetch_profile_by_uuid(id, &auth_config).await.ok().flatten();
|
||||
|
||||
if let Some(profile) = mojang_res {
|
||||
server
|
||||
@ -258,8 +262,8 @@ impl CommandExecutor for ResolveNameExecutor {
|
||||
let name_owned = name.to_string();
|
||||
|
||||
let name_component = TextComponent::text(name_owned.clone());
|
||||
rayon::spawn(move || {
|
||||
let result = fetch_profile_by_name_helper(&server, &name_owned);
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_profile_by_name_helper(&server, &name_owned).await;
|
||||
match result {
|
||||
Some(profile) => {
|
||||
report_resolved_profile(
|
||||
@ -292,8 +296,8 @@ impl CommandExecutor for ResolveIdExecutor {
|
||||
let source = context.source.clone();
|
||||
|
||||
let id_component = TextComponent::text(id.to_string());
|
||||
rayon::spawn(move || {
|
||||
let result = fetch_profile_by_id_helper(&server, id);
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_profile_by_id_helper(&server, id).await;
|
||||
match result {
|
||||
Some(profile) => {
|
||||
report_resolved_profile(
|
||||
|
||||
@ -13,7 +13,7 @@ use pumpkin_util::PermissionLvl;
|
||||
use pumpkin_util::math::vector2::Vector2;
|
||||
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
const DESCRIPTION: &str = "Changes biomes of an area.";
|
||||
const PERMISSION: &str = "minecraft:command.fillbiome";
|
||||
@ -72,8 +72,8 @@ impl CommandExecutor for FillBiomeExecutor {
|
||||
None
|
||||
};
|
||||
|
||||
let mut chunk_modifications: HashMap<Vector2<i32>, Vec<(usize, usize, usize)>> =
|
||||
HashMap::new();
|
||||
let mut chunk_modifications: FxHashMap<Vector2<i32>, Vec<(usize, usize, usize)>> =
|
||||
FxHashMap::default();
|
||||
for y in biome_min_y..=biome_max_y {
|
||||
for z in biome_min_z..=biome_max_z {
|
||||
for x in biome_min_x..=biome_max_x {
|
||||
|
||||
@ -51,7 +51,14 @@ fn fetch_all_contributors_cached() -> Vec<Contributor> {
|
||||
return cache.data.clone();
|
||||
}
|
||||
|
||||
let contributors = fetch_all_contributors();
|
||||
let contributors = tokio::runtime::Handle::try_current().map_or_else(
|
||||
|_| {
|
||||
tokio::runtime::Runtime::new()
|
||||
.map_or_else(|_| Vec::new(), |rt| rt.block_on(fetch_all_contributors()))
|
||||
},
|
||||
|handle| tokio::task::block_in_place(|| handle.block_on(fetch_all_contributors())),
|
||||
);
|
||||
|
||||
if !contributors.is_empty() {
|
||||
if let Ok(mut guard) = CONTRIBUTORS_CACHE.lock() {
|
||||
*guard = Some(ContributorCache {
|
||||
@ -68,25 +75,35 @@ fn fetch_all_contributors_cached() -> Vec<Contributor> {
|
||||
contributors
|
||||
}
|
||||
|
||||
fn fetch_all_contributors() -> Vec<Contributor> {
|
||||
async fn fetch_all_contributors() -> Vec<Contributor> {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("Pumpkin-MC")
|
||||
.build()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut all_contributors = Vec::new();
|
||||
let mut next_url = Some(
|
||||
"https://api.github.com/repos/Pumpkin-MC/Pumpkin/contributors?per_page=100".to_string(),
|
||||
);
|
||||
|
||||
while let Some(url) = next_url {
|
||||
let response = ureq::get(&url).header("User-Agent", "Pumpkin-MC").call();
|
||||
let response = client.get(&url).send().await;
|
||||
|
||||
match response {
|
||||
Ok(mut res) => {
|
||||
if let Ok(contributors) = res.body_mut().read_json::<Vec<Contributor>>() {
|
||||
Ok(res) => {
|
||||
let link_header = res
|
||||
.headers()
|
||||
.get("link")
|
||||
.and_then(|s| s.to_str().ok())
|
||||
.map(String::from);
|
||||
|
||||
if let Ok(contributors) = res.json::<Vec<Contributor>>().await {
|
||||
all_contributors.extend(contributors);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
let link_header = res.headers().get("link").map(|s| s.to_str().unwrap_or(""));
|
||||
|
||||
next_url = link_header.and_then(extract_next_url);
|
||||
next_url = link_header.as_deref().and_then(extract_next_url);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
@ -172,14 +189,18 @@ fn tier_color(tier_slug: &str, tier_name: &str) -> NamedColor {
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_donators_hover() -> TextComponent {
|
||||
async fn fetch_donators_hover() -> TextComponent {
|
||||
let url = "https://market.pumpkinmc.org/api/v1/rest/donators";
|
||||
let response = ureq::get(url).header("User-Agent", "Pumpkin-MC").call();
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("Pumpkin-MC")
|
||||
.build()
|
||||
.unwrap_or_default();
|
||||
let response = client.get(url).send().await;
|
||||
|
||||
let mut donators_text = TextComponent::text("Click to open Donate\n\nDonators:\n");
|
||||
|
||||
if let Ok(mut res) = response
|
||||
&& let Ok(data) = res.body_mut().read_json::<DonatorResponse>()
|
||||
if let Ok(res) = response
|
||||
&& let Ok(data) = res.json::<DonatorResponse>().await
|
||||
{
|
||||
let mut current = data.current;
|
||||
current.sort_by(|a, b| {
|
||||
@ -225,7 +246,16 @@ fn fetch_donators_hover_cached() -> TextComponent {
|
||||
return cache.data.clone();
|
||||
}
|
||||
|
||||
let donators = fetch_donators_hover();
|
||||
let donators = tokio::runtime::Handle::try_current().map_or_else(
|
||||
|_| {
|
||||
tokio::runtime::Runtime::new().map_or_else(
|
||||
|_| TextComponent::text("Unable to load donators"),
|
||||
|rt| rt.block_on(fetch_donators_hover()),
|
||||
)
|
||||
},
|
||||
|handle| tokio::task::block_in_place(|| handle.block_on(fetch_donators_hover())),
|
||||
);
|
||||
|
||||
if let Ok(mut guard) = DONATORS_CACHE.lock() {
|
||||
*guard = Some(DonatorCache {
|
||||
fetched_at: Instant::now(),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::HashMap;
|
||||
use std::{env, fs};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -29,8 +29,8 @@ struct UserCacheEntryDisk {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UserCache {
|
||||
profiles_by_name: HashMap<String, UserCacheEntry>,
|
||||
profiles_by_uuid: HashMap<Uuid, UserCacheEntry>,
|
||||
profiles_by_name: FxHashMap<String, UserCacheEntry>,
|
||||
profiles_by_uuid: FxHashMap<Uuid, UserCacheEntry>,
|
||||
operation_count: u64,
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use pumpkin_data::attributes::Attributes;
|
||||
use pumpkin_data::entity::EntityType;
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
@ -193,10 +193,10 @@ impl Clone for AttributeInstance {
|
||||
}
|
||||
|
||||
/// Registry storing per-entity-type base attribute overrides.
|
||||
/// Internally stores a map from `entity_type.id` -> `HashMap`<attribute.id, f64> for O(1) lookup.
|
||||
/// Internally stores a map from `entity_type.id` -> `FxHashMap`<attribute.id, f64> for O(1) lookup.
|
||||
#[derive(Default)]
|
||||
pub struct AttributeRegistry {
|
||||
map: HashMap<u16, HashMap<u8, f64>>,
|
||||
map: FxHashMap<u16, FxHashMap<u8, f64>>,
|
||||
}
|
||||
|
||||
impl AttributeRegistry {
|
||||
@ -278,7 +278,7 @@ mod tests {
|
||||
.find(|(attr, _)| attr.id == Attributes::MOVEMENT_SPEED.id);
|
||||
assert!(speed_attr.is_some());
|
||||
let (_, base_speed) = speed_attr.unwrap();
|
||||
assert!((base_speed - 0.1).abs() < f64::EPSILON);
|
||||
assert!((base_speed - 0.1).abs() < 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use pumpkin_data::Block;
|
||||
use pumpkin_data::damage::DamageType;
|
||||
use pumpkin_data::world::WorldEvent;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_world::world::BlockFlags;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::entity::effect::MobEffect;
|
||||
use crate::entity::living::LivingEntity;
|
||||
@ -25,7 +24,7 @@ impl MobEffect for WeavingMobEffect {
|
||||
let center_pos = living.entity.block_pos.load();
|
||||
let cobweb_count = (rand::random::<u32>() % 2 + 2) as usize; // 2 to 3 cobwebs
|
||||
|
||||
let mut positions_to_transform = HashSet::new();
|
||||
let mut positions_to_transform = FxHashSet::default();
|
||||
|
||||
// Sample up to 15 random positions in a cube of radius 1
|
||||
for _ in 0..15 {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering},
|
||||
@ -27,7 +27,7 @@ pub struct LightningBoltEntity {
|
||||
flashes: AtomicI32,
|
||||
visual_only: AtomicBool,
|
||||
cause: Mutex<Option<Arc<Player>>>,
|
||||
hit_entities: Mutex<HashSet<i32>>,
|
||||
hit_entities: Mutex<FxHashSet<i32>>,
|
||||
blocks_set_on_fire: AtomicI32,
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ impl LightningBoltEntity {
|
||||
flashes: AtomicI32::new(flashes),
|
||||
visual_only: AtomicBool::new(false),
|
||||
cause: Mutex::new(None),
|
||||
hit_entities: Mutex::new(HashSet::new()),
|
||||
hit_entities: Mutex::new(FxHashSet::default()),
|
||||
blocks_set_on_fire: AtomicI32::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,13 +12,14 @@ use pumpkin_protocol::codec::var_ulong::VarULong;
|
||||
use pumpkin_util::GameMode;
|
||||
use pumpkin_util::Hand;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::atomic::{
|
||||
AtomicBool, AtomicU8,
|
||||
Ordering::{Relaxed, SeqCst},
|
||||
};
|
||||
use std::{collections::HashMap, sync::atomic::AtomicI32};
|
||||
use tracing::warn;
|
||||
|
||||
use super::experience_orb::ExperienceOrbEntity;
|
||||
@ -59,7 +60,7 @@ use pumpkin_protocol::java::client::play::{
|
||||
};
|
||||
use pumpkin_protocol::{
|
||||
codec::item_stack_seralizer::ItemStackSerializer,
|
||||
java::client::play::{CDamageEvent, CSetEquipment, Metadata, MetadataSerializer},
|
||||
java::client::play::{CSetEquipment, Metadata, MetadataSerializer},
|
||||
ser::{NetworkWriteExt, WritingError},
|
||||
};
|
||||
use pumpkin_util::math::boundingbox::BoundingBox;
|
||||
@ -90,11 +91,11 @@ pub struct LivingEntity {
|
||||
pub dead: AtomicBool,
|
||||
/// The distance the entity has been falling.
|
||||
pub fall_distance: AtomicCell<f32>,
|
||||
pub active_effects: std::sync::Mutex<HashMap<&'static StatusEffect, Effect>>,
|
||||
pub active_effects: std::sync::Mutex<FxHashMap<&'static StatusEffect, Effect>>,
|
||||
pub entity_equipment: Arc<std::sync::Mutex<EntityEquipment>>,
|
||||
pub equipment_drop_chances: Arc<std::sync::Mutex<HashMap<EquipmentSlot, f32>>>,
|
||||
pub equipment_drop_chances: Arc<std::sync::Mutex<FxHashMap<EquipmentSlot, f32>>>,
|
||||
pub movement_input: AtomicCell<Vector3<f64>>,
|
||||
pub equipment_slots: Arc<HashMap<usize, EquipmentSlot>>,
|
||||
pub equipment_slots: Arc<FxHashMap<usize, EquipmentSlot>>,
|
||||
|
||||
pub jumping: AtomicBool,
|
||||
|
||||
@ -119,7 +120,7 @@ pub struct LivingEntity {
|
||||
livings_flags: AtomicU8,
|
||||
|
||||
/// The attributes of the entity
|
||||
pub attributes: RwLock<HashMap<u8, AttributeInstance>>,
|
||||
pub attributes: RwLock<FxHashMap<u8, AttributeInstance>>,
|
||||
}
|
||||
|
||||
struct EffectParticle {
|
||||
@ -186,7 +187,7 @@ impl LivingEntity {
|
||||
Self {
|
||||
// Populate local attribute instances from the default registry and get initial vars
|
||||
attributes: {
|
||||
let mut m = std::collections::HashMap::new();
|
||||
let mut m = FxHashMap::default();
|
||||
|
||||
for (attr, base) in entity.entity_type.attributes {
|
||||
if attr.id == Attributes::MAX_HEALTH.id {
|
||||
@ -208,9 +209,9 @@ impl LivingEntity {
|
||||
item_in_use: std::sync::Mutex::new(None),
|
||||
active_hand: std::sync::Mutex::new(None),
|
||||
livings_flags: AtomicU8::new(0),
|
||||
active_effects: std::sync::Mutex::new(HashMap::new()),
|
||||
active_effects: std::sync::Mutex::new(FxHashMap::default()),
|
||||
entity_equipment: Arc::new(std::sync::Mutex::new(EntityEquipment::new())),
|
||||
equipment_drop_chances: Arc::new(std::sync::Mutex::new(HashMap::new())),
|
||||
equipment_drop_chances: Arc::new(std::sync::Mutex::new(FxHashMap::default())),
|
||||
equipment_slots: Arc::new(build_equipment_slots()),
|
||||
jumping: AtomicBool::new(false),
|
||||
jumping_cooldown: AtomicU8::new(0),
|
||||
@ -258,8 +259,8 @@ impl LivingEntity {
|
||||
selected_slot: 0,
|
||||
container_id: window_id,
|
||||
};
|
||||
self.entity.world.load().broadcast_packet_except_editioned(
|
||||
&[self.entity.entity_uuid],
|
||||
self.entity.world.load().send_to_tracking_players_editioned(
|
||||
&self.entity,
|
||||
&je_packet,
|
||||
&be_packet,
|
||||
);
|
||||
@ -271,7 +272,7 @@ impl LivingEntity {
|
||||
self.entity
|
||||
.world
|
||||
.load()
|
||||
.broadcast_packet_except(&[self.entity.entity_uuid], &je_packet);
|
||||
.send_to_tracking_players(&self.entity, &je_packet);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2485,20 +2486,21 @@ impl LivingEntity {
|
||||
data: VarInt(0),
|
||||
fire_at_position: None,
|
||||
};
|
||||
world.broadcast_to_chunk(
|
||||
self.entity.chunk_pos.load(),
|
||||
&CHurtAnimation::new(entity_id.into(), hurt_yaw),
|
||||
let hurt_animation = CHurtAnimation::new(entity_id.into(), hurt_yaw);
|
||||
world.send_to_tracking_players_and_self_editioned(
|
||||
&self.entity,
|
||||
&hurt_animation,
|
||||
&hurt_event,
|
||||
);
|
||||
world.broadcast_to_chunk_bedrock(self.entity.chunk_pos.load(), &hurt_event);
|
||||
}
|
||||
|
||||
world.broadcast_packet_all(&CDamageEvent::new(
|
||||
self.entity.entity_id.into(),
|
||||
damage_type.id.into(),
|
||||
source.map(|e| e.get_entity().entity_id.into()),
|
||||
cause.map(|e| e.get_entity().entity_id.into()),
|
||||
world.broadcast_damage_event(
|
||||
&self.entity,
|
||||
i32::from(damage_type.id),
|
||||
source.map(|e| e.get_entity().entity_id),
|
||||
cause.map(|e| e.get_entity().entity_id),
|
||||
position,
|
||||
));
|
||||
);
|
||||
|
||||
if play_sound {
|
||||
world.play_sound(
|
||||
|
||||
@ -2989,12 +2989,24 @@ impl Entity {
|
||||
bedrock_meta: Option<&SyncedActorDataList>,
|
||||
) {
|
||||
let world = self.world.load();
|
||||
let chunk_pos = self.chunk_pos.load();
|
||||
let players = world.players.load();
|
||||
|
||||
let mut java_recipients = Vec::new();
|
||||
let mut bedrock_recipients = Vec::new();
|
||||
|
||||
if let Some(tracked) = world.entity_tracker.get_tracked_entity(self.entity_id) {
|
||||
for player in players.iter() {
|
||||
if tracked.seen_by.contains(&player.gameprofile.id)
|
||||
|| player.entity_id() == self.entity_id
|
||||
{
|
||||
match player.client.as_ref() {
|
||||
ClientPlatform::Java(_) => java_recipients.push(player),
|
||||
ClientPlatform::Bedrock(client) => bedrock_recipients.push(client),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let chunk_pos = self.chunk_pos.load();
|
||||
for player in players.iter() {
|
||||
let center = player.get_entity().chunk_pos.load();
|
||||
let view_distance = crate::world::chunker::get_view_distance(player).get() as i32;
|
||||
@ -3006,6 +3018,7 @@ impl Entity {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let recipients_by_version =
|
||||
World::collect_java_recipients_by_version(java_recipients.into_iter());
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering};
|
||||
use std::sync::{Arc, Weak};
|
||||
use uuid::Uuid;
|
||||
@ -264,7 +264,7 @@ pub struct VillagerEntity {
|
||||
pub restocks_today: AtomicI32,
|
||||
pub last_gossip_decay_time: AtomicI64,
|
||||
pub last_gossip_share_time: AtomicI64,
|
||||
pub gossips: std::sync::Mutex<HashMap<Uuid, HashMap<GossipType, i32>>>,
|
||||
pub gossips: std::sync::Mutex<FxHashMap<Uuid, FxHashMap<GossipType, i32>>>,
|
||||
pub inventory: std::sync::Mutex<Vec<ItemStack>>,
|
||||
pub merchant_inventory: Arc<SimpleInventory>,
|
||||
pub offers: std::sync::Mutex<Vec<pumpkin_protocol::java::client::play::MerchantOffer>>,
|
||||
@ -333,7 +333,7 @@ impl VillagerEntity {
|
||||
restocks_today: AtomicI32::new(0),
|
||||
last_gossip_decay_time: AtomicI64::new(0),
|
||||
last_gossip_share_time: AtomicI64::new(0),
|
||||
gossips: std::sync::Mutex::new(HashMap::new()),
|
||||
gossips: std::sync::Mutex::new(FxHashMap::default()),
|
||||
inventory,
|
||||
merchant_inventory: Arc::new(SimpleInventory::new(3)),
|
||||
offers: std::sync::Mutex::new(Vec::new()),
|
||||
|
||||
@ -2,7 +2,7 @@ pub mod advancement;
|
||||
pub mod statistics;
|
||||
|
||||
use core::f32;
|
||||
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::f64::consts::TAU;
|
||||
use std::num::NonZero;
|
||||
use std::str::FromStr;
|
||||
@ -41,7 +41,7 @@ use pumpkin_protocol::bedrock::server::{
|
||||
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
|
||||
use pumpkin_util::translation::Locale;
|
||||
use pumpkin_util::version::JavaMinecraftVersion;
|
||||
use pumpkin_world::chunk::{ChunkData, ChunkEntityData};
|
||||
use pumpkin_world::chunk::ChunkData;
|
||||
use pumpkin_world::inventory::Inventory;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, warn};
|
||||
@ -284,7 +284,6 @@ use pumpkin_util::text::hover::HoverEvent;
|
||||
use pumpkin_util::{GameMode, Hand};
|
||||
use pumpkin_world::biome;
|
||||
use pumpkin_world::cylindrical_chunk_iterator::Cylindrical;
|
||||
use pumpkin_world::level::{Level, SyncChunk, SyncEntityChunk};
|
||||
|
||||
use crate::block;
|
||||
use crate::block::blocks::bed::BedBlock;
|
||||
@ -312,7 +311,6 @@ use super::item::ItemEntity;
|
||||
use super::living::LivingEntity;
|
||||
use super::{Entity, EntityBase, NBTStorage, NBTStorageInit};
|
||||
use pumpkin_data::potion::Effect;
|
||||
use pumpkin_world::chunk_system::ChunkLoading;
|
||||
const MAX_CACHED_SIGNATURES: u8 = 128; // Vanilla: 128
|
||||
const MAX_PREVIOUS_MESSAGES: u8 = 20; // Vanilla: 20
|
||||
|
||||
@ -373,324 +371,6 @@ const fn bedrock_inventory_slot(player_screen_slot: i16) -> Option<u32> {
|
||||
}
|
||||
}
|
||||
|
||||
struct HeapNode(i32, Vector2<i32>, Weak<ChunkData>);
|
||||
|
||||
impl Eq for HeapNode {}
|
||||
|
||||
impl PartialEq<Self> for HeapNode {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<Self> for HeapNode {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for HeapNode {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.0.cmp(&other.0).reverse()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChunkManager {
|
||||
chunks_per_tick: usize,
|
||||
center: Vector2<i32>,
|
||||
view_distance: u8,
|
||||
chunk_listener: Receiver<(Vector2<i32>, Weak<ChunkData>)>,
|
||||
chunk_sent: HashMap<Vector2<i32>, Weak<ChunkData>>,
|
||||
chunk_queue: BinaryHeap<HeapNode>,
|
||||
entity_chunk_queue: VecDeque<(Vector2<i32>, Weak<ChunkEntityData>)>,
|
||||
batches_sent_since_ack: u8,
|
||||
last_chunk_batch_sent_at: Instant,
|
||||
/// The (view, simulation) chunk loading ticket levels currently held at `center`.
|
||||
held_tickets: Option<(i8, i8)>,
|
||||
/// The current world for chunk loading. Updated on dimension change.
|
||||
world: Arc<World>,
|
||||
}
|
||||
|
||||
impl ChunkManager {
|
||||
pub const NOTCHIAN_BATCHES_WITHOUT_ACK_UNTIL_PAUSE: u8 = 16;
|
||||
const ACK_STALL_FALLBACK_DELAY: Duration = Duration::from_millis(100);
|
||||
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
chunks_per_tick: usize,
|
||||
chunk_listener: Receiver<(Vector2<i32>, Weak<ChunkData>)>,
|
||||
world: Arc<World>,
|
||||
) -> Self {
|
||||
Self {
|
||||
chunks_per_tick,
|
||||
center: Vector2::<i32>::new(0, 0),
|
||||
view_distance: 0,
|
||||
chunk_listener,
|
||||
chunk_sent: HashMap::new(),
|
||||
chunk_queue: BinaryHeap::new(),
|
||||
entity_chunk_queue: VecDeque::new(),
|
||||
batches_sent_since_ack: 0,
|
||||
last_chunk_batch_sent_at: Instant::now(),
|
||||
held_tickets: None,
|
||||
world,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current world for chunk loading.
|
||||
#[must_use]
|
||||
pub const fn world(&self) -> &Arc<World> {
|
||||
&self.world
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn sent_chunks_count(&self) -> usize {
|
||||
self.chunk_sent.len()
|
||||
}
|
||||
|
||||
pub fn reset_sent_chunks(&mut self) {
|
||||
self.chunk_sent.clear();
|
||||
}
|
||||
|
||||
pub const fn set_view_distance(&mut self, view_distance: u8) {
|
||||
self.view_distance = view_distance;
|
||||
}
|
||||
|
||||
pub fn clean(&mut self) {
|
||||
let (_rx, tx) = crossbeam::channel::unbounded();
|
||||
// drop old channel
|
||||
self.chunk_listener = tx;
|
||||
|
||||
// Drop any held chunk references to allow chunks to be unloaded.
|
||||
self.chunk_sent.clear();
|
||||
self.chunk_queue.clear();
|
||||
self.entity_chunk_queue.clear();
|
||||
}
|
||||
|
||||
fn should_enqueue_chunk(&mut self, position: Vector2<i32>, chunk: &SyncChunk) -> bool {
|
||||
self.chunk_sent
|
||||
.insert(position, Arc::downgrade(chunk))
|
||||
.and_then(|old_chunk| old_chunk.upgrade())
|
||||
.is_none_or(|old_chunk| !Arc::ptr_eq(&old_chunk, chunk))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
const fn ack_window_open(&self) -> bool {
|
||||
self.batches_sent_since_ack < Self::NOTCHIAN_BATCHES_WITHOUT_ACK_UNTIL_PAUSE
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn ack_fallback_ready(&self) -> bool {
|
||||
!self.ack_window_open()
|
||||
&& self.last_chunk_batch_sent_at.elapsed() >= Self::ACK_STALL_FALLBACK_DELAY
|
||||
}
|
||||
|
||||
pub fn pull_new_chunks(&mut self) {
|
||||
while let Ok((pos, chunk_weak)) = self.chunk_listener.try_recv() {
|
||||
let dst = Self::chebyshev(pos, self.center);
|
||||
if dst > i32::from(self.view_distance) {
|
||||
continue;
|
||||
}
|
||||
if let Some(chunk) = chunk_weak.upgrade()
|
||||
&& self.should_enqueue_chunk(pos, &chunk)
|
||||
{
|
||||
self.chunk_queue.push(HeapNode(dst, pos, chunk_weak));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn chebyshev(a: Vector2<i32>, b: Vector2<i32>) -> i32 {
|
||||
(a.x - b.x).abs().max((a.y - b.y).abs())
|
||||
}
|
||||
|
||||
pub fn update_center_and_view_distance(
|
||||
&mut self,
|
||||
center: Vector2<i32>,
|
||||
mut view_distance: u8,
|
||||
level: &Arc<Level>,
|
||||
loading_chunks: &[Vector2<i32>],
|
||||
unloading_chunks: &[Vector2<i32>],
|
||||
) {
|
||||
view_distance += 1; // Margin for loading
|
||||
let old_center = self.center;
|
||||
|
||||
{
|
||||
let mut lock = level
|
||||
.chunk_loading
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let new_level = ChunkLoading::get_level_from_view_distance(view_distance);
|
||||
lock.add_ticket(center, new_level);
|
||||
|
||||
let sim_dist = self.world.server.upgrade().map_or(10, |s| {
|
||||
s.advanced_config.networking.java.simulation_distance.get()
|
||||
});
|
||||
let sim_level = ChunkLoading::get_level_from_simulation_distance(sim_dist);
|
||||
lock.add_ticket(center, sim_level);
|
||||
|
||||
// Drop exactly the pair we last added rather than recomputing it. The
|
||||
// simulation level is derived from live config and the view level from the
|
||||
// previous view distance, so recomputing them can release a ticket we never
|
||||
// took and strand the one we did.
|
||||
if let Some((held_view, held_sim)) = self.held_tickets.replace((new_level, sim_level)) {
|
||||
lock.remove_ticket(old_center, held_view);
|
||||
lock.remove_ticket(old_center, held_sim);
|
||||
}
|
||||
lock.send_change();
|
||||
};
|
||||
|
||||
self.center = center;
|
||||
self.view_distance = view_distance;
|
||||
let view_distance_i32 = i32::from(view_distance);
|
||||
let unloading_chunks: HashSet<Vector2<i32>> = unloading_chunks.iter().copied().collect();
|
||||
|
||||
self.chunk_sent.retain(|pos, _| {
|
||||
(pos.x - center.x).abs().max((pos.y - center.y).abs()) <= view_distance_i32
|
||||
&& !unloading_chunks.contains(pos)
|
||||
});
|
||||
|
||||
self.entity_chunk_queue.retain(|(pos, _)| {
|
||||
(pos.x - center.x).abs().max((pos.y - center.y).abs()) <= view_distance_i32
|
||||
&& !unloading_chunks.contains(pos)
|
||||
});
|
||||
|
||||
let mut tasks: Vec<_> = self
|
||||
.chunk_queue
|
||||
.drain()
|
||||
.filter_map(|node| {
|
||||
let dst = Self::chebyshev(node.1, center);
|
||||
(dst <= view_distance_i32 && !unloading_chunks.contains(&node.1))
|
||||
.then(|| HeapNode(dst, node.1, node.2))
|
||||
})
|
||||
.collect();
|
||||
|
||||
for pos in loading_chunks {
|
||||
if !self.chunk_sent.contains_key(pos)
|
||||
&& let Some(chunk) = level.loaded_chunks.get(pos)
|
||||
{
|
||||
let chunk = chunk.value().clone();
|
||||
if self.should_enqueue_chunk(*pos, &chunk) {
|
||||
let dst = (pos.x - center.x).abs().max((pos.y - center.y).abs());
|
||||
tasks.push(HeapNode(dst, *pos, Arc::downgrade(&chunk)));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.chunk_queue = BinaryHeap::from(tasks);
|
||||
}
|
||||
|
||||
pub fn clean_up(&mut self, level: &Arc<Level>) {
|
||||
let mut lock = level
|
||||
.chunk_loading
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some((view_level, sim_level)) = self.held_tickets.take() {
|
||||
lock.remove_ticket(self.center, view_level);
|
||||
lock.remove_ticket(self.center, sim_level);
|
||||
}
|
||||
lock.send_change();
|
||||
level.should_unload.store(true, Ordering::Relaxed);
|
||||
level.level_channel.notify();
|
||||
let (_rx, tx) = crossbeam::channel::unbounded();
|
||||
// drop old channel
|
||||
self.chunk_listener = tx;
|
||||
|
||||
// Drop any held chunk references to allow chunks to be unloaded.
|
||||
self.chunk_sent.clear();
|
||||
self.chunk_queue.clear();
|
||||
self.entity_chunk_queue.clear();
|
||||
self.batches_sent_since_ack = 0;
|
||||
self.last_chunk_batch_sent_at = Instant::now();
|
||||
self.view_distance = 0;
|
||||
}
|
||||
|
||||
pub fn change_world(&mut self, old_level: &Arc<Level>, new_world: Arc<World>) {
|
||||
let mut lock = old_level
|
||||
.chunk_loading
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some((view_level, sim_level)) = self.held_tickets.take() {
|
||||
lock.remove_ticket(self.center, view_level);
|
||||
lock.remove_ticket(self.center, sim_level);
|
||||
}
|
||||
lock.send_change();
|
||||
old_level.should_unload.store(true, Ordering::Relaxed);
|
||||
old_level.level_channel.notify();
|
||||
drop(lock);
|
||||
self.chunk_listener = new_world.level.chunk_listener.add_global_chunk_listener();
|
||||
self.chunk_sent.clear();
|
||||
self.chunk_queue.clear();
|
||||
self.entity_chunk_queue.clear();
|
||||
self.view_distance = 0;
|
||||
self.world = new_world;
|
||||
// Reset batch state so chunks can be sent immediately in the new dimension
|
||||
self.batches_sent_since_ack = 0;
|
||||
self.last_chunk_batch_sent_at = Instant::now();
|
||||
}
|
||||
|
||||
pub const fn handle_acknowledge(&mut self, chunks_per_tick: f32) {
|
||||
self.batches_sent_since_ack = 0;
|
||||
self.chunks_per_tick = chunks_per_tick.ceil() as usize;
|
||||
}
|
||||
|
||||
pub fn push_chunk(&mut self, position: Vector2<i32>, chunk: &SyncChunk) {
|
||||
if self.should_enqueue_chunk(position, chunk) {
|
||||
let dst = (position.x - self.center.x)
|
||||
.abs()
|
||||
.max((position.y - self.center.y).abs());
|
||||
self.chunk_queue
|
||||
.push(HeapNode(dst, position, Arc::downgrade(chunk)));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_entity(&mut self, position: Vector2<i32>, chunk: &SyncEntityChunk) {
|
||||
self.entity_chunk_queue
|
||||
.push_back((position, Arc::downgrade(chunk)));
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn can_send_chunk(&self) -> bool {
|
||||
let state_available = self.ack_window_open() || self.ack_fallback_ready();
|
||||
|
||||
state_available && !self.chunk_queue.is_empty()
|
||||
}
|
||||
|
||||
pub fn next_chunk(&mut self) -> Box<[SyncChunk]> {
|
||||
let take = self.chunk_queue.len().min(self.chunks_per_tick.max(1));
|
||||
let mut chunks = Vec::with_capacity(take);
|
||||
while chunks.len() < take
|
||||
&& let Some(node) = self.chunk_queue.pop()
|
||||
{
|
||||
if let Some(chunk) = node.2.upgrade() {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
self.batches_sent_since_ack = self.batches_sent_since_ack.saturating_add(1);
|
||||
self.last_chunk_batch_sent_at = Instant::now();
|
||||
|
||||
chunks.into_boxed_slice()
|
||||
}
|
||||
|
||||
pub fn next_entity(&mut self) -> Box<[SyncEntityChunk]> {
|
||||
let chunk_size = self
|
||||
.entity_chunk_queue
|
||||
.len()
|
||||
.min(self.chunks_per_tick.max(1));
|
||||
|
||||
let mut chunks = Vec::with_capacity(chunk_size);
|
||||
while chunks.len() < chunk_size
|
||||
&& let Some((_, weak_chunk)) = self.entity_chunk_queue.pop_front()
|
||||
{
|
||||
if let Some(chunk) = weak_chunk.upgrade() {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
self.batches_sent_since_ack = self.batches_sent_since_ack.saturating_add(1);
|
||||
self.last_chunk_batch_sent_at = Instant::now();
|
||||
|
||||
chunks.into_boxed_slice()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a Minecraft player entity.
|
||||
///
|
||||
/// A `Player` is a special type of entity that represents a human player connected to the server.
|
||||
@ -798,7 +478,10 @@ pub struct Player {
|
||||
pub experience_points: AtomicI32,
|
||||
pub item_cooldowns: std::sync::Mutex<HashMap<String, ItemCooldown>>,
|
||||
pub experience_pick_up_delay: Mutex<u32>,
|
||||
pub chunk_manager: Mutex<ChunkManager>,
|
||||
pub chunk_sender: Mutex<crate::net::ChunkSender>,
|
||||
pub chunk_listener: Mutex<Receiver<(Vector2<i32>, Weak<ChunkData>)>>,
|
||||
pub held_chunk_tickets: Mutex<Option<(i8, i8)>>,
|
||||
pub chunk_send_epoch: AtomicU32,
|
||||
pub has_played_before: AtomicBool,
|
||||
root_vehicle_uuid: AtomicCell<Option<Uuid>>,
|
||||
pub chat_session: Arc<Mutex<ChatSession>>,
|
||||
@ -836,7 +519,6 @@ pub struct Player {
|
||||
use base64::prelude::*;
|
||||
use pumpkin_protocol::Property;
|
||||
use serde::Deserialize;
|
||||
use std::io::Read;
|
||||
|
||||
// Bit masks for the Java skin pixels that Bedrock requires to be opaque.
|
||||
// Adapted from Geyser's SkinProvider under the MIT License.
|
||||
@ -883,10 +565,20 @@ impl Player {
|
||||
.and_then(|m| m.model.as_deref())
|
||||
.is_some_and(|model| model == "slim");
|
||||
|
||||
let resp = ureq::get(&url).call().ok()?;
|
||||
let mut buf = Vec::new();
|
||||
resp.into_body().into_reader().read_to_end(&mut buf).ok()?;
|
||||
let img = image::load_from_memory(&buf).ok()?;
|
||||
let bytes = if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
tokio::task::block_in_place(|| {
|
||||
handle.block_on(async {
|
||||
let client = reqwest::Client::new();
|
||||
client.get(&url).send().await.ok()?.bytes().await.ok()
|
||||
})
|
||||
})?
|
||||
} else {
|
||||
tokio::runtime::Runtime::new().ok()?.block_on(async {
|
||||
let client = reqwest::Client::new();
|
||||
client.get(&url).send().await.ok()?.bytes().await.ok()
|
||||
})?
|
||||
};
|
||||
let img = image::load_from_memory(&bytes).ok()?;
|
||||
|
||||
let width = img.width();
|
||||
let height = img.height();
|
||||
@ -1081,12 +773,10 @@ impl Player {
|
||||
experience_progress: AtomicCell::new(0.0),
|
||||
experience_points: AtomicI32::new(0),
|
||||
item_cooldowns: std::sync::Mutex::new(HashMap::new()),
|
||||
// Default to sending 16 chunks per tick.
|
||||
chunk_manager: Mutex::new(ChunkManager::new(
|
||||
16,
|
||||
world.level.chunk_listener.add_global_chunk_listener(),
|
||||
world.clone(),
|
||||
)),
|
||||
chunk_sender: Mutex::new(crate::net::ChunkSender::new()),
|
||||
chunk_listener: Mutex::new(world.level.chunk_listener.add_global_chunk_listener()),
|
||||
held_chunk_tickets: Mutex::new(None),
|
||||
chunk_send_epoch: AtomicU32::new(0),
|
||||
last_sent_xp: AtomicI32::new(-1),
|
||||
last_sent_health: AtomicI32::new(-1),
|
||||
last_sent_food: AtomicU8::new(0),
|
||||
@ -1341,10 +1031,10 @@ impl Player {
|
||||
world.remove_player(self, true).await;
|
||||
|
||||
let cylindrical = self.watched_section.load();
|
||||
self.chunk_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clean_up(&world.level);
|
||||
self.clean_up_chunk_tickets(&world.level);
|
||||
if let Ok(mut sender) = self.chunk_sender.lock() {
|
||||
sender.reset();
|
||||
}
|
||||
|
||||
// Radial chunks are all of the chunks the player is theoretically viewing.
|
||||
// Given enough time, all of these chunks will be in memory.
|
||||
@ -1396,6 +1086,41 @@ impl Player {
|
||||
.add_passenger(vehicle.clone(), self.clone());
|
||||
}
|
||||
|
||||
pub fn clean_up_chunk_tickets(&self, level: &Arc<pumpkin_world::level::Level>) {
|
||||
let mut lock = level
|
||||
.chunk_loading
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let held = self
|
||||
.held_chunk_tickets
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.take();
|
||||
if let Some((view_level, sim_level)) = held {
|
||||
let center = self.get_entity().chunk_pos.load();
|
||||
lock.remove_ticket(center, view_level);
|
||||
lock.remove_ticket(center, sim_level);
|
||||
}
|
||||
lock.send_change();
|
||||
level.should_unload.store(true, Ordering::Relaxed);
|
||||
level.level_channel.notify();
|
||||
}
|
||||
|
||||
pub fn change_world_chunks(
|
||||
&self,
|
||||
old_level: &Arc<pumpkin_world::level::Level>,
|
||||
new_world: &Arc<crate::world::World>,
|
||||
) {
|
||||
self.clean_up_chunk_tickets(old_level);
|
||||
if let Ok(mut listener) = self.chunk_listener.lock() {
|
||||
*listener = new_world.level.chunk_listener.add_global_chunk_listener();
|
||||
}
|
||||
self.chunk_send_epoch.fetch_add(1, Ordering::Relaxed);
|
||||
if let Ok(mut sender) = self.chunk_sender.lock() {
|
||||
sender.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
pub fn attack(&self, victim: &Arc<dyn EntityBase>) {
|
||||
let world = self.world();
|
||||
@ -2602,35 +2327,60 @@ impl Player {
|
||||
{
|
||||
*xp -= 1;
|
||||
}
|
||||
let (chunk_of_chunks, total_sent_chunks) = self.chunk_manager.try_lock().map_or_else(
|
||||
|_| (None, 0),
|
||||
|mut chunk_manager| {
|
||||
chunk_manager.pull_new_chunks();
|
||||
let chunks = if let ClientPlatform::Java(java_client) = self.client.as_ref() {
|
||||
if java_client.version.load() >= JavaMinecraftVersion::V_1_20_2 {
|
||||
// Java clients (1.20.2+) use the chunk batching protocol.
|
||||
// If we have sent too many chunks without receiving an ack, we stop sending chunks.
|
||||
chunk_manager
|
||||
.can_send_chunk()
|
||||
.then(|| chunk_manager.next_chunk())
|
||||
} else {
|
||||
// Java clients < 1.20.2 do not have chunk batching/ack packets.
|
||||
// Send chunks every tick directly based on chunks_per_tick.
|
||||
(!chunk_manager.chunk_queue.is_empty()).then(|| chunk_manager.next_chunk())
|
||||
}
|
||||
} else {
|
||||
(!chunk_manager.chunk_queue.is_empty()).then(|| chunk_manager.next_chunk())
|
||||
};
|
||||
(chunks, chunk_manager.sent_chunks_count())
|
||||
},
|
||||
);
|
||||
if let Some(chunk_of_chunks) = chunk_of_chunks
|
||||
&& !chunk_of_chunks.is_empty()
|
||||
if let Ok(listener) = self.chunk_listener.try_lock()
|
||||
&& let Ok(mut sender) = self.chunk_sender.try_lock()
|
||||
{
|
||||
let center = self.get_entity().chunk_pos.load();
|
||||
let view_dist =
|
||||
std::num::NonZeroI32::from(self.watched_section.load().view_distance).get();
|
||||
while let Ok((pos, _)) = listener.try_recv() {
|
||||
if (pos.x - center.x).abs().max((pos.y - center.y).abs()) <= view_dist {
|
||||
sender.enqueue_chunk(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let world = self.world();
|
||||
let player_chunk = self.get_entity().chunk_pos.load();
|
||||
let epoch = self.chunk_send_epoch.load(Ordering::Relaxed);
|
||||
let version = match self.client.as_ref() {
|
||||
ClientPlatform::Java(java_client) => java_client.version.load(),
|
||||
ClientPlatform::Bedrock(_) => JavaMinecraftVersion::V_1_20_2,
|
||||
};
|
||||
|
||||
let prepared_batch = self.chunk_sender.try_lock().ok().and_then(|mut sender| {
|
||||
sender.prepare_batch(&world.level, player_chunk, epoch, version)
|
||||
});
|
||||
|
||||
let total_sent_chunks = if let Some(batch) = prepared_batch {
|
||||
match self.client.as_ref() {
|
||||
ClientPlatform::Java(_) => {
|
||||
let mut per_player_cache = rustc_hash::FxHashMap::default();
|
||||
let encoded =
|
||||
crate::net::ChunkSender::encode_batch(&batch, &mut per_player_cache);
|
||||
let current_epoch = self.chunk_send_epoch.load(Ordering::Relaxed);
|
||||
self.chunk_sender.try_lock().map_or(0, |mut sender| {
|
||||
sender.commit_batch(&batch, &encoded, &self.client, current_epoch);
|
||||
sender.sent_chunks_count()
|
||||
})
|
||||
}
|
||||
ClientPlatform::Bedrock(_) => {
|
||||
let chunks: Vec<_> = batch.chunks.into_iter().map(|c| c.chunk).collect();
|
||||
let client = self.client.clone();
|
||||
self.spawn_task(async move {
|
||||
client.send_chunks(&chunk_of_chunks).await;
|
||||
client.send_chunks(&chunks).await;
|
||||
});
|
||||
self.chunk_sender
|
||||
.try_lock()
|
||||
.map_or(0, |s| s.sent_chunks_count())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.chunk_sender
|
||||
.try_lock()
|
||||
.map_or(0, |s| s.sent_chunks_count())
|
||||
};
|
||||
|
||||
if let ClientPlatform::Bedrock(bedrock_client) = self.client.as_ref()
|
||||
&& !self.bedrock_spawned.load(Ordering::Relaxed)
|
||||
&& total_sent_chunks > 4
|
||||
@ -2645,7 +2395,6 @@ impl Player {
|
||||
self.send_bedrock_respawn_state(RespawnState::SearchingForSpawn);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tick_counter.fetch_add(1, Ordering::Relaxed);
|
||||
self.living_entity
|
||||
.entity
|
||||
@ -3656,10 +3405,7 @@ impl Player {
|
||||
});
|
||||
self.unload_watched_chunks(¤t_world).await;
|
||||
|
||||
self.chunk_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.change_world(¤t_world.level, new_world.clone());
|
||||
self.change_world_chunks(¤t_world.level, &new_world);
|
||||
self.living_entity.entity.set_world(new_world.clone());
|
||||
|
||||
if new_world.dimension == pumpkin_data::dimension::Dimension::THE_NETHER {
|
||||
@ -3779,6 +3525,7 @@ impl Player {
|
||||
}
|
||||
|
||||
let i = self.teleport_id_count.fetch_add(1, Ordering::Relaxed);
|
||||
self.chunk_send_epoch.fetch_add(1, Ordering::Relaxed);
|
||||
let teleport_id = i + 1;
|
||||
self.living_entity.entity.set_pos(position);
|
||||
let entity = &self.living_entity.entity;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
pub use pumpkin_data::statistic::{CustomStatistic, StatisticCategory};
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_nbt::tag::NbtTag;
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Statistics {
|
||||
/// (Category ID, Statistic ID) -> Value
|
||||
pub stats: HashMap<(i32, i32), i32>,
|
||||
pub stats: FxHashMap<(i32, i32), i32>,
|
||||
}
|
||||
|
||||
impl Statistics {
|
||||
|
||||
@ -3,11 +3,11 @@ use std::{collections::HashMap, net::IpAddr};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use pumpkin_config::{AuthenticationConfig, networking::auth::TextureConfig};
|
||||
use pumpkin_protocol::Property;
|
||||
use reqwest::{StatusCode, Url};
|
||||
use rsa::RsaPublicKey;
|
||||
use rsa::pkcs8::DecodePublicKey;
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use ureq::http::{StatusCode, Uri};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::GameProfile;
|
||||
@ -54,16 +54,16 @@ const MOJANG_PROFILE_BY_NAME_URL: &str =
|
||||
const MOJANG_PROFILE_BY_UUID_URL: &str =
|
||||
"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false";
|
||||
|
||||
fn create_agent(auth_config: &AuthenticationConfig) -> ureq::Agent {
|
||||
let config = ureq::Agent::config_builder()
|
||||
.timeout_connect(Some(std::time::Duration::from_millis(
|
||||
fn create_client(auth_config: &AuthenticationConfig) -> reqwest::Client {
|
||||
reqwest::Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_millis(
|
||||
auth_config.connect_timeout as u64,
|
||||
)))
|
||||
.timeout_recv_response(Some(std::time::Duration::from_millis(
|
||||
))
|
||||
.timeout(std::time::Duration::from_millis(
|
||||
auth_config.read_timeout as u64,
|
||||
)))
|
||||
.build();
|
||||
config.into()
|
||||
))
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn format_auth_url(url_template: &str, username: &str, server_hash: &str, ip: &IpAddr) -> String {
|
||||
@ -87,7 +87,7 @@ fn format_auth_url(url_template: &str, username: &str, server_hash: &str, ip: &I
|
||||
/// 3. Pumpkin attempts to authenticate the player against the primary auth server, falling back to secondary auth servers if the primary is down.
|
||||
///
|
||||
/// See <https://pumpkinmc.org/developer/networking/authentication>
|
||||
pub fn authenticate(
|
||||
pub async fn authenticate(
|
||||
username: &str,
|
||||
server_hash: &str,
|
||||
ip: &IpAddr,
|
||||
@ -111,7 +111,7 @@ pub fn authenticate(
|
||||
candidate_urls.push(fallback.as_str());
|
||||
}
|
||||
|
||||
let agent = create_agent(auth_config);
|
||||
let client = create_client(auth_config);
|
||||
|
||||
let mut unverified_count = 0;
|
||||
let mut last_unknown_status = None;
|
||||
@ -119,7 +119,7 @@ pub fn authenticate(
|
||||
for url_template in candidate_urls {
|
||||
let address = format_auth_url(url_template, username, server_hash, ip);
|
||||
|
||||
let mut response = match agent.get(&address).call() {
|
||||
let response = match client.get(&address).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
@ -136,7 +136,7 @@ pub fn authenticate(
|
||||
}
|
||||
|
||||
match status {
|
||||
StatusCode::OK => match response.body_mut().read_json::<GameProfile>() {
|
||||
StatusCode::OK => match response.json::<GameProfile>().await {
|
||||
Ok(profile) => return Ok(profile),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse GameProfile response from '{address}': {err}");
|
||||
@ -167,41 +167,35 @@ pub fn validate_textures(property: &Property, config: &TextureConfig) -> Result<
|
||||
let textures: ProfileTextures =
|
||||
serde_json::from_slice(&from64).map_err(|e| TextureError::JSONError(e.to_string()))?;
|
||||
for texture in textures.textures {
|
||||
let url = texture
|
||||
.1
|
||||
.url
|
||||
.parse()
|
||||
.map_err(|_| TextureError::InvalidURL)?;
|
||||
let url = Url::parse(&texture.1.url).map_err(|_| TextureError::InvalidURL)?;
|
||||
is_texture_url_valid(&url, config)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_texture_url_valid(url: &Uri, config: &TextureConfig) -> Result<(), TextureError> {
|
||||
let Some(scheme) = url.scheme() else {
|
||||
return Err(TextureError::InvalidURL);
|
||||
};
|
||||
pub fn is_texture_url_valid(url: &Url, config: &TextureConfig) -> Result<(), TextureError> {
|
||||
let scheme = url.scheme();
|
||||
if !config
|
||||
.allowed_url_schemes
|
||||
.iter()
|
||||
.any(|allowed_scheme| scheme.as_str().ends_with(allowed_scheme))
|
||||
.any(|allowed_scheme| scheme.ends_with(allowed_scheme))
|
||||
{
|
||||
return Err(TextureError::DisallowedUrlScheme(scheme.to_string()));
|
||||
}
|
||||
let Some(domain) = url.authority() else {
|
||||
let Some(domain) = url.domain() else {
|
||||
return Err(TextureError::InvalidURL);
|
||||
};
|
||||
if !config
|
||||
.allowed_url_domains
|
||||
.iter()
|
||||
.any(|allowed_domain| domain.as_str().ends_with(allowed_domain))
|
||||
.any(|allowed_domain| domain.ends_with(allowed_domain))
|
||||
{
|
||||
return Err(TextureError::DisallowedUrlDomain(domain.to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fetch_mojang_public_keys(
|
||||
pub async fn fetch_mojang_public_keys(
|
||||
auth_config: &AuthenticationConfig,
|
||||
) -> Result<Vec<RsaPublicKey>, AuthError> {
|
||||
let services_url = auth_config
|
||||
@ -211,10 +205,11 @@ pub fn fetch_mojang_public_keys(
|
||||
|
||||
let url = format!("{services_url}/publickeys");
|
||||
|
||||
let agent = create_agent(auth_config);
|
||||
let mut response = agent
|
||||
let client = create_client(auth_config);
|
||||
let response = client
|
||||
.get(&url)
|
||||
.call()
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| AuthError::FailedResponse)?;
|
||||
|
||||
match response.status() {
|
||||
@ -223,10 +218,8 @@ pub fn fetch_mojang_public_keys(
|
||||
other => Err(AuthError::UnknownStatusCode(other))?,
|
||||
}
|
||||
|
||||
let public_keys: MojangPublicKeys = response
|
||||
.body_mut()
|
||||
.read_json()
|
||||
.map_err(|_| AuthError::FailedParse)?;
|
||||
let public_keys: MojangPublicKeys =
|
||||
response.json().await.map_err(|_| AuthError::FailedParse)?;
|
||||
|
||||
let as_rsa_keys = public_keys
|
||||
.player_certificate_keys
|
||||
@ -248,7 +241,7 @@ struct MojangProfileByNameResponse {
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub fn lookup_profile_by_name(
|
||||
pub async fn lookup_profile_by_name(
|
||||
name: &str,
|
||||
auth_config: &AuthenticationConfig,
|
||||
) -> Result<Option<(Uuid, String)>, AuthError> {
|
||||
@ -263,7 +256,7 @@ pub fn lookup_profile_by_name(
|
||||
candidate_urls.push(fallback.as_str());
|
||||
}
|
||||
|
||||
let agent = create_agent(auth_config);
|
||||
let client = create_client(auth_config);
|
||||
|
||||
let mut not_found_count = 0;
|
||||
let mut last_unknown_status = None;
|
||||
@ -271,7 +264,7 @@ pub fn lookup_profile_by_name(
|
||||
for url_template in candidate_urls {
|
||||
let address = url_template.replace("{username}", name);
|
||||
|
||||
let mut response = match agent.get(&address).call() {
|
||||
let response = match client.get(&address).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
@ -288,10 +281,7 @@ pub fn lookup_profile_by_name(
|
||||
}
|
||||
|
||||
match status {
|
||||
StatusCode::OK => match response
|
||||
.body_mut()
|
||||
.read_json::<MojangProfileByNameResponse>()
|
||||
{
|
||||
StatusCode::OK => match response.json::<MojangProfileByNameResponse>().await {
|
||||
Ok(profile) => {
|
||||
let parsed_uuid =
|
||||
Uuid::parse_str(&profile.id).map_err(|_| AuthError::FailedParse)?;
|
||||
@ -321,7 +311,20 @@ pub fn lookup_profile_by_name(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_profile_by_uuid(
|
||||
pub fn lookup_profile_by_name_blocking(
|
||||
name: &str,
|
||||
auth_config: &AuthenticationConfig,
|
||||
) -> Result<Option<(Uuid, String)>, AuthError> {
|
||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
tokio::task::block_in_place(|| handle.block_on(lookup_profile_by_name(name, auth_config)))
|
||||
} else {
|
||||
tokio::runtime::Runtime::new()
|
||||
.map_err(|_e| AuthError::FailedParse)?
|
||||
.block_on(lookup_profile_by_name(name, auth_config))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_profile_by_uuid(
|
||||
uuid: Uuid,
|
||||
auth_config: &AuthenticationConfig,
|
||||
) -> Result<Option<GameProfile>, AuthError> {
|
||||
@ -336,7 +339,7 @@ pub fn fetch_profile_by_uuid(
|
||||
candidate_urls.push(fallback.as_str());
|
||||
}
|
||||
|
||||
let agent = create_agent(auth_config);
|
||||
let client = create_client(auth_config);
|
||||
|
||||
let mut not_found_count = 0;
|
||||
let mut last_unknown_status = None;
|
||||
@ -348,7 +351,7 @@ pub fn fetch_profile_by_uuid(
|
||||
.replace("{uuid}", &uuid_simple)
|
||||
.replace("{uuid_hyphenated}", &uuid.to_string());
|
||||
|
||||
let mut response = match agent.get(&address).call() {
|
||||
let response = match client.get(&address).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(err) => {
|
||||
tracing::warn!("Profile fetch server at '{address}' is down or unreachable: {err}");
|
||||
@ -363,7 +366,7 @@ pub fn fetch_profile_by_uuid(
|
||||
}
|
||||
|
||||
match status {
|
||||
StatusCode::OK => match response.body_mut().read_json::<GameProfile>() {
|
||||
StatusCode::OK => match response.json::<GameProfile>().await {
|
||||
Ok(profile) => return Ok(Some(profile)),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse GameProfile response from '{address}': {err}");
|
||||
|
||||
311
crates/pumpkin/src/net/chunk_sender.rs
Normal file
311
crates/pumpkin/src/net/chunk_sender.rs
Normal file
@ -0,0 +1,311 @@
|
||||
use bytes::Bytes;
|
||||
use rayon::prelude::*;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use pumpkin_protocol::codec::var_int::VarInt;
|
||||
use pumpkin_protocol::java::client::play::{
|
||||
CChunkBatchEnd, CChunkBatchStart, CChunkData, CLightUpdate, CUnloadChunk,
|
||||
};
|
||||
use pumpkin_protocol::ser::NetworkWriteExt;
|
||||
use pumpkin_protocol::{ClientPacket, MultiVersionJavaPacket};
|
||||
use pumpkin_util::math::vector2::Vector2;
|
||||
use pumpkin_util::version::JavaMinecraftVersion;
|
||||
use pumpkin_world::chunk::ChunkData;
|
||||
use pumpkin_world::level::{Level, SyncChunk};
|
||||
|
||||
use crate::net::ClientPlatform;
|
||||
|
||||
const MIN_CHUNKS_PER_TICK: f32 = 0.1;
|
||||
const MAX_CHUNKS_PER_TICK: f32 = 500.0;
|
||||
const INITIAL_CHUNKS_PER_TICK: f32 = 9.0;
|
||||
const MAX_CONCURRENT_BATCHES: u16 = 10;
|
||||
|
||||
pub struct PreparedChunk {
|
||||
pub position: Vector2<i32>,
|
||||
pub chunk: SyncChunk,
|
||||
}
|
||||
|
||||
pub struct PreparedBatch {
|
||||
pub chunks: Vec<PreparedChunk>,
|
||||
pub epoch_snapshot: u32,
|
||||
pub target_version: JavaMinecraftVersion,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EncodedChunk {
|
||||
pub position: Vector2<i32>,
|
||||
pub payload: Bytes,
|
||||
pub light_payload: Option<Bytes>,
|
||||
pub chunk_ref: Weak<ChunkData>,
|
||||
}
|
||||
|
||||
impl EncodedChunk {
|
||||
#[must_use]
|
||||
pub fn is_fresh_for(&self, candidate: &PreparedChunk) -> bool {
|
||||
let Some(held) = self.chunk_ref.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
self.position == candidate.position && Arc::ptr_eq(&held, &candidate.chunk)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChunkSender {
|
||||
pub pending_chunks: FxHashSet<Vector2<i32>>,
|
||||
sent_chunks: FxHashSet<Vector2<i32>>,
|
||||
pub in_flight_batches: u16,
|
||||
pub desired_rate: f32,
|
||||
pub send_quota: f32,
|
||||
pub max_in_flight: u16,
|
||||
}
|
||||
|
||||
impl ChunkSender {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pending_chunks: FxHashSet::default(),
|
||||
sent_chunks: FxHashSet::default(),
|
||||
in_flight_batches: 0,
|
||||
desired_rate: INITIAL_CHUNKS_PER_TICK,
|
||||
send_quota: 0.0,
|
||||
max_in_flight: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.pending_chunks.clear();
|
||||
self.sent_chunks.clear();
|
||||
self.in_flight_batches = 0;
|
||||
self.send_quota = 0.0;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_chunk_sent(&self, pos: &Vector2<i32>) -> bool {
|
||||
self.sent_chunks.contains(pos)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn sent_chunks_count(&self) -> usize {
|
||||
self.sent_chunks.len()
|
||||
}
|
||||
|
||||
pub const fn on_batch_acknowledged(&mut self, client_requested_rate: f32) -> bool {
|
||||
if self.in_flight_batches == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.in_flight_batches = self.in_flight_batches.saturating_sub(1);
|
||||
self.desired_rate = if client_requested_rate.is_nan() {
|
||||
MIN_CHUNKS_PER_TICK
|
||||
} else {
|
||||
client_requested_rate.clamp(MIN_CHUNKS_PER_TICK, MAX_CHUNKS_PER_TICK)
|
||||
};
|
||||
|
||||
if self.in_flight_batches == 0 {
|
||||
self.send_quota = 1.0;
|
||||
}
|
||||
|
||||
self.max_in_flight = MAX_CONCURRENT_BATCHES;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn enqueue_chunk(&mut self, pos: Vector2<i32>) {
|
||||
self.sent_chunks.remove(&pos);
|
||||
self.pending_chunks.insert(pos);
|
||||
}
|
||||
|
||||
pub fn unload_chunk(&mut self, client: &ClientPlatform, pos: Vector2<i32>) {
|
||||
self.pending_chunks.remove(&pos);
|
||||
if self.sent_chunks.remove(&pos)
|
||||
&& let ClientPlatform::Java(java_client) = client
|
||||
&& !java_client.is_closed()
|
||||
{
|
||||
java_client.try_send_packet(&CUnloadChunk::new(pos.x, pos.y));
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_sorted_candidates(&self, level: &Level, center: Vector2<i32>) -> Vec<PreparedChunk> {
|
||||
let quota_limit = self.send_quota.floor() as usize;
|
||||
let mut sorted: Vec<Vector2<i32>> = self.pending_chunks.iter().copied().collect();
|
||||
|
||||
sorted.sort_by_key(|pos| {
|
||||
let dx = (pos.x - center.x).unsigned_abs() as u64;
|
||||
let dz = (pos.y - center.y).unsigned_abs() as u64;
|
||||
dx * dx + dz * dz
|
||||
});
|
||||
|
||||
let mut ready = Vec::with_capacity(quota_limit);
|
||||
for pos in sorted {
|
||||
if ready.len() >= quota_limit {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(chunk) = level.loaded_chunks.get(&pos) {
|
||||
ready.push(PreparedChunk {
|
||||
position: pos,
|
||||
chunk: chunk.value().clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ready
|
||||
}
|
||||
|
||||
pub fn prepare_batch(
|
||||
&mut self,
|
||||
level: &Level,
|
||||
player_chunk: Vector2<i32>,
|
||||
epoch: u32,
|
||||
version: JavaMinecraftVersion,
|
||||
) -> Option<PreparedBatch> {
|
||||
if self.in_flight_batches >= self.max_in_flight {
|
||||
return None;
|
||||
}
|
||||
|
||||
let max_batch = self.desired_rate.max(1.0);
|
||||
self.send_quota = (self.send_quota + self.desired_rate).min(max_batch);
|
||||
|
||||
if self.send_quota < 1.0 || self.pending_chunks.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let candidates = self.collect_sorted_candidates(level, player_chunk);
|
||||
if candidates.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(PreparedBatch {
|
||||
chunks: candidates,
|
||||
epoch_snapshot: epoch,
|
||||
target_version: version,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode_batch(
|
||||
batch: &PreparedBatch,
|
||||
cache: &mut FxHashMap<Vector2<i32>, EncodedChunk>,
|
||||
) -> Vec<EncodedChunk> {
|
||||
let version = batch.target_version;
|
||||
let cached_map = &*cache;
|
||||
|
||||
let encoded_results: Vec<Option<EncodedChunk>> = batch
|
||||
.chunks
|
||||
.par_iter()
|
||||
.map(|candidate| {
|
||||
let pos = candidate.position;
|
||||
if let Some(cached) = cached_map.get(&pos)
|
||||
&& cached.is_fresh_for(candidate)
|
||||
{
|
||||
return Some(cached.clone());
|
||||
}
|
||||
|
||||
let chunk = &candidate.chunk;
|
||||
let mut chunk_buf = Vec::with_capacity(32 * 1024);
|
||||
if chunk_buf
|
||||
.write_var_int(&VarInt(CChunkData::to_id(version)))
|
||||
.is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if CChunkData(chunk)
|
||||
.write_packet_data(&mut chunk_buf, &version)
|
||||
.is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let light_payload = if version >= JavaMinecraftVersion::V_1_14
|
||||
&& version < JavaMinecraftVersion::V_1_18
|
||||
{
|
||||
CLightUpdate::from_chunk(chunk, version)
|
||||
.ok()
|
||||
.and_then(|light_packet| {
|
||||
let mut light_buf = Vec::new();
|
||||
(light_buf
|
||||
.write_var_int(&VarInt(CLightUpdate::to_id(version)))
|
||||
.is_ok()
|
||||
&& light_packet
|
||||
.write_packet_data(&mut light_buf, &version)
|
||||
.is_ok())
|
||||
.then(|| Bytes::from(light_buf))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Some(EncodedChunk {
|
||||
position: pos,
|
||||
payload: Bytes::from(chunk_buf),
|
||||
light_payload,
|
||||
chunk_ref: Arc::downgrade(chunk),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut output = Vec::with_capacity(encoded_results.len());
|
||||
for encoded in encoded_results.into_iter().flatten() {
|
||||
cache.insert(encoded.position, encoded.clone());
|
||||
output.push(encoded);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn commit_batch(
|
||||
&mut self,
|
||||
batch: &PreparedBatch,
|
||||
encoded_chunks: &[EncodedChunk],
|
||||
client: &ClientPlatform,
|
||||
current_epoch: u32,
|
||||
) -> Vec<Vector2<i32>> {
|
||||
if current_epoch != batch.epoch_snapshot || encoded_chunks.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut dispatched_positions = Vec::with_capacity(encoded_chunks.len());
|
||||
let version = batch.target_version;
|
||||
|
||||
if version >= JavaMinecraftVersion::V_1_20_2
|
||||
&& let ClientPlatform::Java(java_client) = client
|
||||
{
|
||||
java_client.try_send_packet(&CChunkBatchStart);
|
||||
}
|
||||
|
||||
for chunk in encoded_chunks {
|
||||
if !self.pending_chunks.contains(&chunk.position) {
|
||||
continue;
|
||||
}
|
||||
|
||||
client.try_enqueue_packet(chunk.payload.clone());
|
||||
if let Some(ref light) = chunk.light_payload {
|
||||
client.try_enqueue_packet(light.clone());
|
||||
}
|
||||
|
||||
self.pending_chunks.remove(&chunk.position);
|
||||
self.sent_chunks.insert(chunk.position);
|
||||
dispatched_positions.push(chunk.position);
|
||||
}
|
||||
|
||||
let sent_count = dispatched_positions.len();
|
||||
if sent_count > 0 {
|
||||
if version >= JavaMinecraftVersion::V_1_20_2
|
||||
&& let ClientPlatform::Java(java_client) = client
|
||||
{
|
||||
java_client.try_send_packet(&CChunkBatchEnd::new(sent_count as u16));
|
||||
}
|
||||
|
||||
self.in_flight_batches = self.in_flight_batches.saturating_add(1);
|
||||
self.send_quota -= sent_count as f32;
|
||||
}
|
||||
|
||||
dispatched_positions
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChunkSender {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@ -66,7 +66,10 @@ impl PendingConnection {
|
||||
};
|
||||
|
||||
if server.advanced_config.networking.java.online_mode {
|
||||
match self.authenticate(server, &shared_secret, &profile_name) {
|
||||
match self
|
||||
.authenticate(server, &shared_secret, &profile_name)
|
||||
.await
|
||||
{
|
||||
Ok(new_profile) => self.gameprofile = Some(new_profile),
|
||||
Err(error) => {
|
||||
self.kick(match error {
|
||||
@ -168,7 +171,7 @@ impl PendingConnection {
|
||||
Some(PacketHandlerResult::ReadyToPlay(profile.clone(), config))
|
||||
}
|
||||
|
||||
fn authenticate(
|
||||
async fn authenticate(
|
||||
&self,
|
||||
server: &Server,
|
||||
shared_secret: &[u8],
|
||||
@ -181,7 +184,8 @@ impl PendingConnection {
|
||||
&hash,
|
||||
&ip,
|
||||
&server.advanced_config.networking.java.authentication,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(actions) = &profile.profile_actions {
|
||||
if server
|
||||
|
||||
@ -4,10 +4,10 @@ use super::*;
|
||||
impl JavaClient {
|
||||
pub fn handle_chunk_batch(&self, player: &Player, packet: &SChunkBatch) {
|
||||
player
|
||||
.chunk_manager
|
||||
.chunk_sender
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.handle_acknowledge(packet.chunks_per_tick);
|
||||
.on_batch_acknowledged(packet.chunks_per_tick);
|
||||
trace!(
|
||||
"Client requested {} chunks per tick",
|
||||
packet.chunks_per_tick
|
||||
|
||||
@ -28,6 +28,8 @@ use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
pub mod authentication;
|
||||
pub mod bedrock;
|
||||
pub mod chunk_sender;
|
||||
pub use chunk_sender::ChunkSender;
|
||||
pub mod java;
|
||||
pub mod lan_broadcast;
|
||||
pub mod packet_limiter;
|
||||
|
||||
@ -182,15 +182,43 @@ pub fn fetch_market_public_key() -> Result<String, String> {
|
||||
return Ok(cached_key.clone());
|
||||
}
|
||||
|
||||
let mut response = ureq::get(PUMPKIN_MARKET_PUBLIC_KEY_URL)
|
||||
.header("User-Agent", "Pumpkin-MC")
|
||||
.call()
|
||||
let body = if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
tokio::task::block_in_place(|| {
|
||||
handle.block_on(async {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("Pumpkin-MC")
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build HTTP client: {e}"))?;
|
||||
let response = client
|
||||
.get(PUMPKIN_MARKET_PUBLIC_KEY_URL)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch public key from market: {e}"))?;
|
||||
|
||||
let body = response
|
||||
.body_mut()
|
||||
.read_to_string()
|
||||
.map_err(|e| format!("Failed to read public key response: {e}"))?;
|
||||
response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read public key response: {e}"))
|
||||
})
|
||||
})?
|
||||
} else {
|
||||
tokio::runtime::Runtime::new()
|
||||
.map_err(|e| format!("Failed to create runtime: {e}"))?
|
||||
.block_on(async {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("Pumpkin-MC")
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build HTTP client: {e}"))?;
|
||||
let response = client
|
||||
.get(PUMPKIN_MARKET_PUBLIC_KEY_URL)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch public key from market: {e}"))?;
|
||||
response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read public key response: {e}"))
|
||||
})?
|
||||
};
|
||||
|
||||
let key = body.trim().trim_matches('"').to_string();
|
||||
if key.is_empty() {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use pumpkin_data::enchantment::AttributeModifierSlot;
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -16,7 +16,7 @@ pub struct CustomEnchantmentEntry {
|
||||
}
|
||||
|
||||
pub struct EnchantmentManager {
|
||||
custom_enchantments: RwLock<HashMap<String, CustomEnchantmentEntry>>,
|
||||
custom_enchantments: RwLock<FxHashMap<String, CustomEnchantmentEntry>>,
|
||||
}
|
||||
|
||||
impl Default for EnchantmentManager {
|
||||
@ -29,7 +29,7 @@ impl EnchantmentManager {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
custom_enchantments: RwLock::new(HashMap::new()),
|
||||
custom_enchantments: RwLock::new(FxHashMap::default()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ use pumpkin_world::world_info::{LevelData, WorldInfoError, WorldInfoReader, Worl
|
||||
use rand::seq::{IndexedRandom, SliceRandom};
|
||||
use rayon::prelude::*;
|
||||
use rsa::RsaPublicKey;
|
||||
use std::collections::HashSet;
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::fs;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
@ -330,7 +330,9 @@ impl Server {
|
||||
.java
|
||||
.authentication
|
||||
.clone();
|
||||
let keys = fetch_mojang_public_keys(&auth_config).unwrap_or_else(|e| {
|
||||
let keys = fetch_mojang_public_keys(&auth_config)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Failed to fetch Mojang keys: {e}");
|
||||
Vec::new()
|
||||
});
|
||||
@ -358,7 +360,9 @@ impl Server {
|
||||
auth.url.as_deref(),
|
||||
auth.connect_timeout,
|
||||
auth.read_timeout,
|
||||
) {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(keys) => keys,
|
||||
Err(error) => {
|
||||
error!("Failed to fetch Bedrock OIDC keys: {error}");
|
||||
@ -1179,7 +1183,7 @@ impl Server {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
.collect::<FxHashSet<_>>();
|
||||
let type_excluded = target_selector
|
||||
.conditions
|
||||
.iter()
|
||||
@ -1190,7 +1194,7 @@ impl Server {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
.collect::<FxHashSet<_>>();
|
||||
|
||||
players.retain(|_| {
|
||||
(type_excluded.is_empty() || !type_excluded.contains(player_type))
|
||||
@ -1290,7 +1294,7 @@ impl Server {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
.collect::<FxHashSet<_>>();
|
||||
let type_excluded = target_selector
|
||||
.conditions
|
||||
.iter()
|
||||
@ -1301,7 +1305,7 @@ impl Server {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
.collect::<FxHashSet<_>>();
|
||||
entities.retain(|entity| {
|
||||
// Filter by entity type
|
||||
(type_excluded.is_empty() || !type_excluded.contains(&entity.get_entity().entity_type))
|
||||
|
||||
@ -3,7 +3,7 @@ use std::{num::NonZero, sync::Arc};
|
||||
|
||||
use pumpkin_protocol::{
|
||||
bedrock::client::network_chunk_publisher_update::CNetworkChunkPublisherUpdate,
|
||||
java::client::play::{CCenterChunk, CUnloadChunk},
|
||||
java::client::play::CCenterChunk,
|
||||
};
|
||||
use pumpkin_world::cylindrical_chunk_iterator::Cylindrical;
|
||||
|
||||
@ -78,30 +78,51 @@ pub fn update_position(player: &Arc<Player>) {
|
||||
let loading_chunks: Vec<_> = loading_iter.collect();
|
||||
let unloading_chunks: Vec<_> = unloading_iter.collect();
|
||||
|
||||
// Use the chunk_manager's world reference, which is updated on dimension change.
|
||||
// This ensures we load chunks from the correct world after portal teleportation.
|
||||
let world = {
|
||||
let mut chunk_manager = player
|
||||
.chunk_manager
|
||||
let world = player.world();
|
||||
let level = &world.level;
|
||||
let mut held_tickets = player
|
||||
.held_chunk_tickets
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let world = chunk_manager.world().clone();
|
||||
chunk_manager.update_center_and_view_distance(
|
||||
new_chunk_center,
|
||||
view_distance.into(),
|
||||
&world.level,
|
||||
&loading_chunks,
|
||||
&unloading_chunks,
|
||||
);
|
||||
world
|
||||
};
|
||||
player.watched_section.store(new_cylindrical);
|
||||
|
||||
if let ClientPlatform::Java(client) = player.client.as_ref() {
|
||||
for chunk in &unloading_chunks {
|
||||
client.try_send_packet(&CUnloadChunk::new(chunk.x, chunk.y));
|
||||
{
|
||||
let mut lock = level
|
||||
.chunk_loading
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let new_level = pumpkin_world::chunk_system::ChunkLoading::get_level_from_view_distance(
|
||||
u8::from(view_distance) + 1,
|
||||
);
|
||||
lock.add_ticket(new_chunk_center, new_level);
|
||||
|
||||
let sim_dist = world.server.upgrade().map_or(10, |s| {
|
||||
s.advanced_config.networking.java.simulation_distance.get()
|
||||
});
|
||||
let sim_level =
|
||||
pumpkin_world::chunk_system::ChunkLoading::get_level_from_simulation_distance(sim_dist);
|
||||
lock.add_ticket(new_chunk_center, sim_level);
|
||||
|
||||
if let Some((held_view, held_sim)) = held_tickets.replace((new_level, sim_level)) {
|
||||
lock.remove_ticket(old_cylindrical.center, held_view);
|
||||
lock.remove_ticket(old_cylindrical.center, held_sim);
|
||||
}
|
||||
lock.send_change();
|
||||
};
|
||||
drop(held_tickets);
|
||||
|
||||
{
|
||||
let mut sender = player
|
||||
.chunk_sender
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
for pos in &unloading_chunks {
|
||||
sender.unload_chunk(&player.client, *pos);
|
||||
}
|
||||
for pos in &loading_chunks {
|
||||
sender.enqueue_chunk(*pos);
|
||||
}
|
||||
}
|
||||
player.watched_section.store(new_cylindrical);
|
||||
|
||||
// Make sure the watched section and the chunk watcher updates are async atomic. We want to
|
||||
// ensure what we unload when the player disconnects is correct.
|
||||
@ -133,4 +154,5 @@ pub fn update_position(player: &Arc<Player>) {
|
||||
if !loading_chunks.is_empty() {
|
||||
world.spawn_world_entity_chunks(player.clone(), loading_chunks, new_chunk_center);
|
||||
}
|
||||
world.entity_tracker.update_player_position(player, &world);
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ use crate::entity::player::Player;
|
||||
use crate::server::Server;
|
||||
use crate::world::bossbar::{Bossbar, BossbarColor, BossbarDivisions};
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
@ -43,7 +43,7 @@ impl CustomBossbar {
|
||||
}
|
||||
|
||||
pub struct CustomBossbars {
|
||||
pub custom_bossbars: HashMap<String, CustomBossbar>,
|
||||
pub custom_bossbars: FxHashMap<String, CustomBossbar>,
|
||||
}
|
||||
|
||||
impl Default for CustomBossbars {
|
||||
@ -56,7 +56,7 @@ impl CustomBossbars {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
custom_bossbars: HashMap::new(),
|
||||
custom_bossbars: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
461
crates/pumpkin/src/world/entity_tracker.rs
Normal file
461
crates/pumpkin/src/world/entity_tracker.rs
Normal file
@ -0,0 +1,461 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::BufMut;
|
||||
use dashmap::DashMap;
|
||||
use dashmap::DashSet;
|
||||
use pumpkin_protocol::bedrock::client::remove_actor::CRemoveActor;
|
||||
use pumpkin_protocol::codec::var_long::VarLong;
|
||||
use pumpkin_protocol::java::client::play::{
|
||||
CEntityVelocity, CHeadRot, CRemoveEntities, CSetEntityMetadata, Metadata,
|
||||
};
|
||||
use pumpkin_protocol::{BClientPacket, ClientPacket};
|
||||
use pumpkin_util::version::JavaMinecraftVersion;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::entity::EntityBase;
|
||||
use crate::entity::player::Player;
|
||||
use crate::net::ClientPlatform;
|
||||
use crate::net::java::JavaClient;
|
||||
use crate::world::World;
|
||||
use crate::world::chunker::{get_view_distance, is_within_view_distance};
|
||||
|
||||
/// Represents an entity tracked in the world and its current watchers,
|
||||
/// corresponding to Vanilla's `ChunkMap.TrackedEntity`.
|
||||
pub struct TrackedEntity {
|
||||
pub entity: Arc<dyn EntityBase>,
|
||||
pub entity_id: i32,
|
||||
pub tracking_range: u32,
|
||||
pub update_interval: u32,
|
||||
pub track_deltas: bool,
|
||||
pub seen_by: DashSet<Uuid>,
|
||||
}
|
||||
|
||||
impl TrackedEntity {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
entity: Arc<dyn EntityBase>,
|
||||
range: u32,
|
||||
update_interval: u32,
|
||||
track_deltas: bool,
|
||||
) -> Self {
|
||||
let entity_id = entity.get_entity().entity_id;
|
||||
Self {
|
||||
entity,
|
||||
entity_id,
|
||||
tracking_range: range,
|
||||
update_interval,
|
||||
track_deltas,
|
||||
seen_by: DashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the effective tracking range in chunks, taking passengers into account.
|
||||
#[must_use]
|
||||
pub fn get_effective_range(&self) -> u32 {
|
||||
let mut effective_range = self.tracking_range;
|
||||
if let Ok(passengers) = self.entity.get_entity().passengers.try_lock() {
|
||||
for passenger in passengers.iter() {
|
||||
let passenger_range = passenger.get_entity().entity_type.client_tracking_range;
|
||||
if passenger_range > effective_range {
|
||||
effective_range = passenger_range;
|
||||
}
|
||||
}
|
||||
}
|
||||
effective_range
|
||||
}
|
||||
|
||||
/// Updates visibility for a single player.
|
||||
pub fn update_player(&self, player: &Arc<Player>, _world: &World) {
|
||||
if player.get_entity().entity_id == self.entity_id {
|
||||
return;
|
||||
}
|
||||
|
||||
let player_entity = player.get_entity();
|
||||
let player_pos = player_entity.pos.load();
|
||||
let entity_pos = self.entity.get_entity().pos.load();
|
||||
let dx = player_pos.x - entity_pos.x;
|
||||
let dz = player_pos.z - entity_pos.z;
|
||||
let dist_sq = dx.mul_add(dx, dz * dz);
|
||||
|
||||
let player_vd = get_view_distance(player).get() as i32;
|
||||
let effective_range = self.get_effective_range();
|
||||
let visible_range_blocks = f64::from((effective_range as i32).min(player_vd) * 16);
|
||||
let range_sq = visible_range_blocks * visible_range_blocks;
|
||||
|
||||
let entity_chunk = self.entity.get_entity().chunk_pos.load();
|
||||
let player_chunk = player_entity.chunk_pos.load();
|
||||
let in_view = is_within_view_distance(entity_chunk, player_chunk, player_vd);
|
||||
|
||||
let is_visible = dist_sq <= range_sq && in_view;
|
||||
|
||||
if is_visible {
|
||||
if self.seen_by.insert(player.gameprofile.id) {
|
||||
self.add_pairing(player);
|
||||
}
|
||||
} else if self.seen_by.remove(&player.gameprofile.id).is_some() {
|
||||
self.remove_pairing(player);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates visibility for a list of players.
|
||||
pub fn update_players(&self, players: &[Arc<Player>], world: &World) {
|
||||
for player in players {
|
||||
self.update_player(player, world);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends spawn and initial state packets to the new watcher.
|
||||
pub fn add_pairing(&self, player: &Arc<Player>) {
|
||||
player.client.try_enqueue_spawn_packet(&self.entity);
|
||||
|
||||
if let Some(target_player) = self.entity.get_player() {
|
||||
let skin_parts = target_player.config.load().skin_parts;
|
||||
let target_entity = target_player.get_entity();
|
||||
let target_id = target_entity.entity_id;
|
||||
|
||||
if let ClientPlatform::Java(client) = player.client.as_ref() {
|
||||
let version = client.version.load();
|
||||
if version >= JavaMinecraftVersion::V_1_21 {
|
||||
let mut buf = Vec::new();
|
||||
for meta in [
|
||||
Metadata::new(
|
||||
pumpkin_data::tracked_data::player::PLAYER_MODE_CUSTOMISATION,
|
||||
skin_parts,
|
||||
),
|
||||
Metadata::new(
|
||||
pumpkin_data::tracked_data::player::PLAYER_MODE_CUSTOMIZATION_ID,
|
||||
skin_parts,
|
||||
),
|
||||
] {
|
||||
let _ = meta.write(&mut buf, &version);
|
||||
}
|
||||
buf.put_u8(255);
|
||||
let meta_packet = CSetEntityMetadata::new(target_id.into(), buf.into());
|
||||
if let Ok(packet_data) =
|
||||
JavaClient::serialize_packet_for_version(&meta_packet, version)
|
||||
{
|
||||
client.try_enqueue_packet(packet_data);
|
||||
}
|
||||
}
|
||||
|
||||
let head_yaw = target_entity.head_yaw.load();
|
||||
let head_rot_packet = CHeadRot::new(
|
||||
target_id.into(),
|
||||
(head_yaw * 256.0 / 360.0).rem_euclid(256.0) as u8,
|
||||
);
|
||||
if let Ok(data) = client.serialize_packet(&head_rot_packet) {
|
||||
client.try_enqueue_packet(data);
|
||||
}
|
||||
}
|
||||
} else if self.entity.get_living_entity().is_some()
|
||||
&& let ClientPlatform::Java(client) = player.client.as_ref()
|
||||
{
|
||||
let head_yaw = self.entity.get_entity().head_yaw.load();
|
||||
let head_rot_packet = CHeadRot::new(
|
||||
self.entity_id.into(),
|
||||
(head_yaw * 256.0 / 360.0).rem_euclid(256.0) as u8,
|
||||
);
|
||||
if let Ok(data) = client.serialize_packet(&head_rot_packet) {
|
||||
client.try_enqueue_packet(data);
|
||||
}
|
||||
}
|
||||
|
||||
let vel = self.entity.get_entity().velocity.load();
|
||||
if vel.length_squared() > 1e-4
|
||||
&& let ClientPlatform::Java(client) = player.client.as_ref()
|
||||
{
|
||||
let motion = CEntityVelocity::new(self.entity_id.into(), vel);
|
||||
if let Ok(data) = client.serialize_packet(&motion) {
|
||||
client.try_enqueue_packet(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends despawn packet to a player leaving visibility range.
|
||||
pub fn remove_pairing(&self, player: &Arc<Player>) {
|
||||
let entity_ids = [self.entity_id.into()];
|
||||
match player.client.as_ref() {
|
||||
ClientPlatform::Java(client) => {
|
||||
let packet = CRemoveEntities::new(&entity_ids);
|
||||
if let Ok(data) = client.serialize_packet(&packet) {
|
||||
client.try_enqueue_packet(data);
|
||||
}
|
||||
}
|
||||
ClientPlatform::Bedrock(client) => {
|
||||
let packet = CRemoveActor::new(VarLong(i64::from(self.entity_id)));
|
||||
if let Ok(data) = client.serialize_packet(&packet) {
|
||||
client.try_enqueue_packet(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Broadcasts removal of this entity to all current watchers.
|
||||
pub fn broadcast_removed(&self, world: &World) {
|
||||
let entity_ids = [self.entity_id.into()];
|
||||
let je_packet = CRemoveEntities::new(&entity_ids);
|
||||
let be_packet = CRemoveActor::new(VarLong(i64::from(self.entity_id)));
|
||||
|
||||
let players = world.players.load();
|
||||
let recipients = players
|
||||
.iter()
|
||||
.filter(|p| self.seen_by.contains(&p.gameprofile.id));
|
||||
|
||||
let mut java_recipients = Vec::new();
|
||||
let mut bedrock_recipients = Vec::new();
|
||||
for p in recipients {
|
||||
match p.client.as_ref() {
|
||||
ClientPlatform::Java(_) => java_recipients.push(p),
|
||||
ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client),
|
||||
}
|
||||
}
|
||||
let recipients_by_version =
|
||||
World::collect_java_recipients_by_version(java_recipients.into_iter());
|
||||
World::broadcast_java_grouped(&je_packet, recipients_by_version);
|
||||
World::broadcast_bedrock_grouped(&be_packet, bedrock_recipients.into_iter());
|
||||
|
||||
self.seen_by.clear();
|
||||
}
|
||||
|
||||
/// Removes a player by UUID without sending despawn packet (used on disconnect).
|
||||
pub fn remove_player(&self, player_uuid: &Uuid) {
|
||||
self.seen_by.remove(player_uuid);
|
||||
}
|
||||
|
||||
/// Sends a Java client packet to all tracking players.
|
||||
pub fn send_to_tracking_players<P: ClientPacket + Sync>(&self, packet: &P, world: &World) {
|
||||
let players = world.players.load();
|
||||
let recipients = players
|
||||
.iter()
|
||||
.filter(|p| self.seen_by.contains(&p.gameprofile.id));
|
||||
let recipients_by_version = World::collect_java_recipients_by_version(recipients);
|
||||
World::broadcast_java_grouped(packet, recipients_by_version);
|
||||
}
|
||||
|
||||
/// Sends a Bedrock client packet to all tracking players.
|
||||
pub fn send_to_tracking_players_bedrock<P: BClientPacket + Sync>(
|
||||
&self,
|
||||
packet: &P,
|
||||
world: &World,
|
||||
) {
|
||||
let players = world.players.load();
|
||||
let recipients = players.iter().filter_map(|p| {
|
||||
if self.seen_by.contains(&p.gameprofile.id)
|
||||
&& let ClientPlatform::Bedrock(client) = p.client.as_ref()
|
||||
{
|
||||
return Some(client);
|
||||
}
|
||||
None
|
||||
});
|
||||
World::broadcast_bedrock_grouped(packet, recipients);
|
||||
}
|
||||
|
||||
/// Sends an editioned packet (Java + Bedrock) to all tracking players.
|
||||
pub fn send_to_tracking_players_editioned<J: ClientPacket + Sync, B: BClientPacket + Sync>(
|
||||
&self,
|
||||
je_packet: &J,
|
||||
be_packet: &B,
|
||||
world: &World,
|
||||
) {
|
||||
let players = world.players.load();
|
||||
let recipients = players
|
||||
.iter()
|
||||
.filter(|p| self.seen_by.contains(&p.gameprofile.id));
|
||||
|
||||
let mut java_recipients = Vec::new();
|
||||
let mut bedrock_recipients = Vec::new();
|
||||
for p in recipients {
|
||||
match p.client.as_ref() {
|
||||
ClientPlatform::Java(_) => java_recipients.push(p),
|
||||
ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client),
|
||||
}
|
||||
}
|
||||
let recipients_by_version =
|
||||
World::collect_java_recipients_by_version(java_recipients.into_iter());
|
||||
World::broadcast_java_grouped(je_packet, recipients_by_version);
|
||||
World::broadcast_bedrock_grouped(be_packet, bedrock_recipients.into_iter());
|
||||
}
|
||||
|
||||
/// Sends a packet to all tracking players and the entity itself if it is a player.
|
||||
pub fn send_to_tracking_players_and_self<P: ClientPacket + Sync>(
|
||||
&self,
|
||||
packet: &P,
|
||||
world: &World,
|
||||
) {
|
||||
self.send_to_tracking_players(packet, world);
|
||||
if let Some(player) = self.entity.get_player() {
|
||||
player.try_send_client_packet(packet);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an editioned packet to all tracking players and the entity itself if it is a player.
|
||||
pub fn send_to_tracking_players_and_self_editioned<
|
||||
J: ClientPacket + Sync,
|
||||
B: BClientPacket + Sync,
|
||||
>(
|
||||
&self,
|
||||
je_packet: &J,
|
||||
be_packet: &B,
|
||||
world: &World,
|
||||
) {
|
||||
self.send_to_tracking_players_editioned(je_packet, be_packet, world);
|
||||
if let Some(player) = self.entity.get_player() {
|
||||
player.try_enqueue_packet_editioned(je_packet, be_packet);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a packet to tracking players filtered by predicate.
|
||||
pub fn send_to_tracking_players_filtered<P: ClientPacket + Sync, F: Fn(&Player) -> bool>(
|
||||
&self,
|
||||
packet: &P,
|
||||
world: &World,
|
||||
filter: F,
|
||||
) {
|
||||
let players = world.players.load();
|
||||
let recipients = players
|
||||
.iter()
|
||||
.filter(|p| self.seen_by.contains(&p.gameprofile.id) && filter(p));
|
||||
let recipients_by_version = World::collect_java_recipients_by_version(recipients);
|
||||
World::broadcast_java_grouped(packet, recipients_by_version);
|
||||
}
|
||||
|
||||
/// Sends an editioned packet to tracking players filtered by predicate.
|
||||
pub fn send_to_tracking_players_filtered_editioned<
|
||||
J: ClientPacket + Sync,
|
||||
B: BClientPacket + Sync,
|
||||
F: Fn(&Player) -> bool,
|
||||
>(
|
||||
&self,
|
||||
je_packet: &J,
|
||||
be_packet: &B,
|
||||
world: &World,
|
||||
filter: F,
|
||||
) {
|
||||
let players = world.players.load();
|
||||
let recipients = players
|
||||
.iter()
|
||||
.filter(|p| self.seen_by.contains(&p.gameprofile.id) && filter(p));
|
||||
|
||||
let mut java_recipients = Vec::new();
|
||||
let mut bedrock_recipients = Vec::new();
|
||||
for p in recipients {
|
||||
match p.client.as_ref() {
|
||||
ClientPlatform::Java(_) => java_recipients.push(p),
|
||||
ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client),
|
||||
}
|
||||
}
|
||||
let recipients_by_version =
|
||||
World::collect_java_recipients_by_version(java_recipients.into_iter());
|
||||
World::broadcast_java_grouped(je_packet, recipients_by_version);
|
||||
World::broadcast_bedrock_grouped(be_packet, bedrock_recipients.into_iter());
|
||||
}
|
||||
}
|
||||
|
||||
/// The entity tracking system for a world, corresponding to Vanilla's `ChunkMap.entityMap`.
|
||||
pub struct EntityTracker {
|
||||
pub entity_map: DashMap<i32, Arc<TrackedEntity>>,
|
||||
}
|
||||
|
||||
impl Default for EntityTracker {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityTracker {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entity_map: DashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_tracked_entity(&self, entity_id: i32) -> Option<Arc<TrackedEntity>> {
|
||||
self.entity_map.get(&entity_id).map(|r| r.value().clone())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn has_entity_with_id(&self, entity_id: i32) -> bool {
|
||||
self.entity_map.contains_key(&entity_id)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_tracked_by_any_player(&self, entity_id: i32) -> bool {
|
||||
self.entity_map
|
||||
.get(&entity_id)
|
||||
.is_some_and(|t| !t.seen_by.is_empty())
|
||||
}
|
||||
|
||||
pub fn add_entity(&self, entity: &Arc<dyn EntityBase>, world: &World) {
|
||||
let entity_type = entity.get_entity().entity_type;
|
||||
let range = entity_type.client_tracking_range;
|
||||
if range == 0 {
|
||||
return;
|
||||
}
|
||||
let update_interval = entity_type.update_interval;
|
||||
let track_deltas = entity_type.track_deltas;
|
||||
let entity_id = entity.get_entity().entity_id;
|
||||
|
||||
let tracked = Arc::new(TrackedEntity::new(
|
||||
entity.clone(),
|
||||
range,
|
||||
update_interval,
|
||||
track_deltas,
|
||||
));
|
||||
self.entity_map.insert(entity_id, tracked.clone());
|
||||
|
||||
let players = world.players.load();
|
||||
tracked.update_players(players.as_ref(), world);
|
||||
|
||||
// If the newly added entity is a player, update all other tracked entities for this player
|
||||
if let Some(player) = entity.get_player()
|
||||
&& let Some(player_arc) = world.get_player_by_uuid(player.gameprofile.id)
|
||||
{
|
||||
for entry in &self.entity_map {
|
||||
if *entry.key() != entity_id {
|
||||
entry.value().update_player(&player_arc, world);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_entity(&self, entity: &dyn EntityBase, world: &World) {
|
||||
let entity_id = entity.get_entity().entity_id;
|
||||
if let Some(player) = entity.get_player() {
|
||||
let player_id = player.gameprofile.id;
|
||||
for entry in &self.entity_map {
|
||||
entry.value().remove_player(&player_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((_, tracked)) = self.entity_map.remove(&entity_id) {
|
||||
tracked.broadcast_removed(world);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_player_position(&self, player: &Arc<Player>, world: &World) {
|
||||
for entry in &self.entity_map {
|
||||
if *entry.key() == player.get_entity().entity_id {
|
||||
let players = world.players.load();
|
||||
entry.value().update_players(players.as_ref(), world);
|
||||
} else {
|
||||
entry.value().update_player(player, world);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_entity_position(&self, entity: &dyn EntityBase, world: &World) {
|
||||
if let Some(tracked) = self.entity_map.get(&entity.get_entity().entity_id) {
|
||||
let players = world.players.load();
|
||||
tracked.update_players(players.as_ref(), world);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_all(&self, world: &World) {
|
||||
let players = world.players.load();
|
||||
for entry in &self.entity_map {
|
||||
entry.value().update_players(players.as_ref(), world);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -116,7 +116,7 @@ use pumpkin_protocol::{
|
||||
java::{
|
||||
self,
|
||||
client::play::{
|
||||
CBlockEntityData, CEntityStatus, CGameEvent, CLogin, CMultiBlockUpdate,
|
||||
CBlockEntityData, CDamageEvent, CEntityStatus, CGameEvent, CLogin, CMultiBlockUpdate,
|
||||
CPlayerChatMessage, CPlayerInfoUpdate, CRemoveEntities, CRemovePlayerInfo,
|
||||
CSetSelectedSlot, CSoundEffect, CSpawnEntity, FilterType, GameEvent, InitChat,
|
||||
PlayerAction, PlayerInfoFlags,
|
||||
@ -160,6 +160,7 @@ pub mod bossbar;
|
||||
pub mod custom_bossbar;
|
||||
pub mod dragon_fight;
|
||||
pub mod end_podium;
|
||||
pub mod entity_tracker;
|
||||
pub mod natural_spawner;
|
||||
pub mod scoreboard;
|
||||
pub mod weather;
|
||||
@ -280,6 +281,8 @@ pub struct World {
|
||||
pub custom_data: std::sync::Mutex<NbtCompound>,
|
||||
/// Persistent custom data for block entities at specific positions
|
||||
pub custom_block_entity_data: DashMap<BlockPos, NbtCompound>,
|
||||
/// Entity tracker responsible for tracking entity visibility and sending delta/status packets to watchers.
|
||||
pub entity_tracker: entity_tracker::EntityTracker,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@ -403,6 +406,7 @@ impl World {
|
||||
block_entities: DashMap::new(),
|
||||
custom_data: std::sync::Mutex::new(custom_data),
|
||||
custom_block_entity_data: DashMap::new(),
|
||||
entity_tracker: entity_tracker::EntityTracker::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -546,14 +550,15 @@ impl World {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an entity status update to all players tracking the specified entity.
|
||||
pub fn send_entity_status(
|
||||
/// Broadcasts an entity status update / event to all players tracking the specified entity,
|
||||
/// and to the entity itself if it is a player.
|
||||
/// Matching Vanilla's `ServerLevel.broadcastEntityEvent(entity, event)`.
|
||||
pub fn broadcast_entity_event(
|
||||
&self,
|
||||
entity: &Entity,
|
||||
java_status: EntityStatus,
|
||||
bedrock_status: Option<ActorEventID>,
|
||||
) {
|
||||
let chunk_pos = entity.chunk_pos.load();
|
||||
let je_packet = CEntityStatus::new(entity.entity_id, java_status as i8);
|
||||
if let Some(be_event) = bedrock_status {
|
||||
let be_packet = SActorEvent {
|
||||
@ -562,14 +567,44 @@ impl World {
|
||||
data: VarInt(0),
|
||||
fire_at_position: None,
|
||||
};
|
||||
self.broadcast_to_chunk_editioned(chunk_pos, &je_packet, &be_packet);
|
||||
self.send_to_tracking_players_and_self_editioned(entity, &je_packet, &be_packet);
|
||||
} else {
|
||||
self.broadcast_to_chunk(chunk_pos, &je_packet);
|
||||
self.send_to_tracking_players_and_self(entity, &je_packet);
|
||||
}
|
||||
}
|
||||
|
||||
/// Broadcasts a damage event to all players tracking the specified entity,
|
||||
/// and to the entity itself if it is a player.
|
||||
/// Matching Vanilla's `ServerLevel.broadcastDamageEvent(entity, source)`.
|
||||
pub fn broadcast_damage_event(
|
||||
&self,
|
||||
entity: &Entity,
|
||||
damage_type_id: i32,
|
||||
source_entity_id: Option<i32>,
|
||||
cause_entity_id: Option<i32>,
|
||||
position: Option<Vector3<f64>>,
|
||||
) {
|
||||
let je_packet = CDamageEvent::new(
|
||||
entity.entity_id.into(),
|
||||
damage_type_id.into(),
|
||||
source_entity_id.map(Into::into),
|
||||
cause_entity_id.map(Into::into),
|
||||
position,
|
||||
);
|
||||
self.send_to_tracking_players_and_self(entity, &je_packet);
|
||||
}
|
||||
|
||||
/// Sends an entity status update to all players tracking the specified entity.
|
||||
pub fn send_entity_status(
|
||||
&self,
|
||||
entity: &Entity,
|
||||
java_status: EntityStatus,
|
||||
bedrock_status: Option<ActorEventID>,
|
||||
) {
|
||||
self.broadcast_entity_event(entity, java_status, bedrock_status);
|
||||
}
|
||||
|
||||
pub fn send_remove_mob_effect(&self, entity: &Entity, effect_type: &'static StatusEffect) {
|
||||
let chunk_pos = entity.chunk_pos.load();
|
||||
let je_packet =
|
||||
CRemoveMobEffect::new(entity.entity_id.into(), VarInt(i32::from(effect_type.id)));
|
||||
|
||||
@ -583,11 +618,10 @@ impl World {
|
||||
tick: VarULong(0),
|
||||
ambient: false,
|
||||
};
|
||||
self.broadcast_to_chunk_editioned(chunk_pos, &je_packet, &be_packet);
|
||||
self.send_to_tracking_players_and_self_editioned(entity, &je_packet, &be_packet);
|
||||
}
|
||||
|
||||
pub fn send_add_mob_effect(&self, entity: &Entity, effect: &pumpkin_data::potion::Effect) {
|
||||
let chunk_pos = entity.chunk_pos.load();
|
||||
let mut flags: i8 = 0;
|
||||
if effect.ambient {
|
||||
flags |= 0x01;
|
||||
@ -618,7 +652,91 @@ impl World {
|
||||
ambient: effect.ambient,
|
||||
};
|
||||
|
||||
self.broadcast_to_chunk_editioned(chunk_pos, &je_packet, &be_packet);
|
||||
self.send_to_tracking_players_and_self_editioned(entity, &je_packet, &be_packet);
|
||||
}
|
||||
|
||||
pub fn send_to_tracking_players<P: ClientPacket + Sync>(&self, entity: &Entity, packet: &P) {
|
||||
if let Some(tracked) = self.entity_tracker.get_tracked_entity(entity.entity_id) {
|
||||
tracked.send_to_tracking_players(packet, self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to_tracking_players_bedrock<P: BClientPacket + Sync>(
|
||||
&self,
|
||||
entity: &Entity,
|
||||
packet: &P,
|
||||
) {
|
||||
if let Some(tracked) = self.entity_tracker.get_tracked_entity(entity.entity_id) {
|
||||
tracked.send_to_tracking_players_bedrock(packet, self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to_tracking_players_editioned<J: ClientPacket + Sync, B: BClientPacket + Sync>(
|
||||
&self,
|
||||
entity: &Entity,
|
||||
je_packet: &J,
|
||||
be_packet: &B,
|
||||
) {
|
||||
if let Some(tracked) = self.entity_tracker.get_tracked_entity(entity.entity_id) {
|
||||
tracked.send_to_tracking_players_editioned(je_packet, be_packet, self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to_tracking_players_and_self<P: ClientPacket + Sync>(
|
||||
&self,
|
||||
entity: &Entity,
|
||||
packet: &P,
|
||||
) {
|
||||
if let Some(tracked) = self.entity_tracker.get_tracked_entity(entity.entity_id) {
|
||||
tracked.send_to_tracking_players_and_self(packet, self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to_tracking_players_and_self_editioned<
|
||||
J: ClientPacket + Sync,
|
||||
B: BClientPacket + Sync,
|
||||
>(
|
||||
&self,
|
||||
entity: &Entity,
|
||||
je_packet: &J,
|
||||
be_packet: &B,
|
||||
) {
|
||||
if let Some(tracked) = self.entity_tracker.get_tracked_entity(entity.entity_id) {
|
||||
tracked.send_to_tracking_players_and_self_editioned(je_packet, be_packet, self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to_tracking_players_filtered<P: ClientPacket + Sync, F: Fn(&Player) -> bool>(
|
||||
&self,
|
||||
entity: &Entity,
|
||||
packet: &P,
|
||||
filter: F,
|
||||
) {
|
||||
if let Some(tracked) = self.entity_tracker.get_tracked_entity(entity.entity_id) {
|
||||
tracked.send_to_tracking_players_filtered(packet, self, filter);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to_tracking_players_filtered_editioned<
|
||||
J: ClientPacket + Sync,
|
||||
B: BClientPacket + Sync,
|
||||
F: Fn(&Player) -> bool,
|
||||
>(
|
||||
&self,
|
||||
entity: &Entity,
|
||||
je_packet: &J,
|
||||
be_packet: &B,
|
||||
filter: F,
|
||||
) {
|
||||
if let Some(tracked) = self.entity_tracker.get_tracked_entity(entity.entity_id) {
|
||||
tracked.send_to_tracking_players_filtered_editioned(je_packet, be_packet, self, filter);
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_tracked_by_any_player(&self, entity: &Entity) -> bool {
|
||||
self.entity_tracker
|
||||
.is_tracked_by_any_player(entity.entity_id)
|
||||
}
|
||||
|
||||
pub fn set_difficulty(&self, difficulty: Difficulty) {
|
||||
@ -1323,6 +1441,8 @@ impl World {
|
||||
});
|
||||
let entity_elapsed = t_entities.elapsed();
|
||||
|
||||
self.entity_tracker.update_all(self);
|
||||
|
||||
let mut block_entities: Vec<Arc<dyn BlockEntity>> = Vec::new();
|
||||
for chunk_pos in active_chunks.iter() {
|
||||
if let Some(chunk_block_entities) = self.block_entities.get(chunk_pos) {
|
||||
@ -3788,13 +3908,7 @@ impl World {
|
||||
container_id: 0,
|
||||
};
|
||||
|
||||
let chunk_pos = from.get_entity().chunk_pos.load();
|
||||
self.broadcast_to_chunk_except_editioned(
|
||||
chunk_pos,
|
||||
&[from.get_entity().entity_uuid],
|
||||
&je_packet,
|
||||
&be_mob_equipment,
|
||||
);
|
||||
self.send_to_tracking_players_editioned(from.get_entity(), &je_packet, &be_mob_equipment);
|
||||
}
|
||||
|
||||
pub fn send_world_info(
|
||||
@ -4095,11 +4209,7 @@ impl World {
|
||||
// observer sees the player in a world whose chunk manager doesn't match.
|
||||
self.remove_player(player, false).await;
|
||||
player.unload_watched_chunks(self).await;
|
||||
player
|
||||
.chunk_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.change_world(&self.level, destination.clone());
|
||||
player.change_world_chunks(&self.level, &destination);
|
||||
player.living_entity.entity.set_world(destination.clone());
|
||||
destination.players.rcu(|current_list| {
|
||||
let mut new_list = (**current_list).clone();
|
||||
@ -4710,6 +4820,8 @@ impl World {
|
||||
new_list.push(player.clone());
|
||||
new_list
|
||||
});
|
||||
self.entity_tracker
|
||||
.add_entity(&(player.clone() as Arc<dyn EntityBase>), self);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -4751,6 +4863,8 @@ impl World {
|
||||
new_list
|
||||
});
|
||||
if let Some(ref player) = removed_player {
|
||||
self.entity_tracker
|
||||
.remove_entity(player.as_ref() as &dyn EntityBase, self);
|
||||
let uuid = player.gameprofile.id;
|
||||
let entity_id = player.entity_id();
|
||||
|
||||
@ -4805,7 +4919,7 @@ impl World {
|
||||
#[expect(clippy::needless_pass_by_value)]
|
||||
pub fn spawn_entity_non_save(&self, entity: Arc<dyn EntityBase>) {
|
||||
let _base_entity = entity.get_entity();
|
||||
self.broadcast_entity_spawn(&entity);
|
||||
self.entity_tracker.add_entity(&entity, self);
|
||||
self.spawn_state.load().add_entity(self, entity.as_ref());
|
||||
|
||||
self.entities.rcu(|current_entities| {
|
||||
@ -4829,7 +4943,6 @@ impl World {
|
||||
return;
|
||||
}
|
||||
|
||||
self.broadcast_entity_spawn(&entity);
|
||||
entity.init_data_tracker();
|
||||
self.add_entity_silent(entity);
|
||||
}
|
||||
@ -4869,6 +4982,7 @@ impl World {
|
||||
// unload (see `save_entity`), never at spawn, so it can't be both live and
|
||||
// serialized at once (which would double it on the next reload).
|
||||
self.spawn_state.load().add_entity(self, entity.as_ref());
|
||||
self.entity_tracker.add_entity(&entity, self);
|
||||
|
||||
self.entities.rcu(|current_entities| {
|
||||
let mut new_entities = (**current_entities).clone();
|
||||
@ -4889,18 +5003,12 @@ impl World {
|
||||
base_entity.removed.store(true, Ordering::Release);
|
||||
|
||||
self.spawn_state.load().remove_entity(self, entity);
|
||||
self.entity_tracker.remove_entity(entity, self);
|
||||
self.entities.rcu(|current_entities| {
|
||||
let mut new_entities = (**current_entities).clone();
|
||||
new_entities.retain(|e| e.get_entity().entity_uuid != base_entity.entity_uuid);
|
||||
new_entities
|
||||
});
|
||||
|
||||
let chunk_pos = base_entity.chunk_pos.load();
|
||||
self.broadcast_to_chunk_editioned(
|
||||
chunk_pos,
|
||||
&CRemoveEntities::new(&[base_entity.entity_id.into()]),
|
||||
&CRemoveActor::new(VarLong(base_entity.entity_id as i64)),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn remove_entities_in_chunks(
|
||||
@ -4929,6 +5037,7 @@ impl World {
|
||||
});
|
||||
|
||||
for entity in entities_to_remove {
|
||||
self.entity_tracker.remove_entity(entity.as_ref(), self);
|
||||
self.save_entity(&entity).await;
|
||||
self.spawn_state.load().remove_entity(self, entity.as_ref());
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ use pumpkin_data::tag::Taggable;
|
||||
use pumpkin_data::tag::WorldgenBiome::MINECRAFT_REDUCE_WATER_AMBIENT_SPAWNS;
|
||||
use pumpkin_data::{Block, BlockDirection, BlockState};
|
||||
use pumpkin_util::GameMode;
|
||||
use pumpkin_util::math::boundingbox::{BoundingBox, EntityDimensions};
|
||||
use pumpkin_util::math::get_section_cord;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector2::Vector2;
|
||||
@ -826,16 +825,10 @@ pub fn is_valid_spawn_position_for_type(
|
||||
if !check_spawn_rules(entity_type, world, block_pos, is_thundering) {
|
||||
return false;
|
||||
}
|
||||
// TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes
|
||||
if !world.is_space_empty(BoundingBox::new_from_pos(
|
||||
if !world.is_space_empty(entity_type.get_spawn_bounding_box(
|
||||
f64::from(block_pos.0.x) + 0.5,
|
||||
f64::from(block_pos.0.y),
|
||||
f64::from(block_pos.0.z) + 0.5,
|
||||
&EntityDimensions {
|
||||
width: entity_type.dimension[0],
|
||||
height: entity_type.dimension[1],
|
||||
eye_height: entity_type.eye_height,
|
||||
},
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use pumpkin_data::scoreboard::ScoreboardDisplaySlot;
|
||||
use pumpkin_protocol::{
|
||||
@ -114,10 +114,10 @@ impl ScoreboardTarget for NoTarget {
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Scoreboard {
|
||||
objectives: HashMap<String, ScoreboardObjective>,
|
||||
display_slots: HashMap<ScoreboardDisplaySlot, String>,
|
||||
scores: HashMap<String, HashMap<String, ScoreboardScore>>,
|
||||
teams: HashMap<String, Team>,
|
||||
objectives: FxHashMap<String, ScoreboardObjective>,
|
||||
display_slots: FxHashMap<ScoreboardDisplaySlot, String>,
|
||||
scores: FxHashMap<String, FxHashMap<String, ScoreboardScore>>,
|
||||
teams: FxHashMap<String, Team>,
|
||||
}
|
||||
|
||||
impl Scoreboard {
|
||||
@ -127,7 +127,7 @@ impl Scoreboard {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_objectives(&self) -> &HashMap<String, ScoreboardObjective> {
|
||||
pub const fn get_objectives(&self) -> &FxHashMap<String, ScoreboardObjective> {
|
||||
&self.objectives
|
||||
}
|
||||
|
||||
@ -141,7 +141,7 @@ impl Scoreboard {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_display_slots(&self) -> &HashMap<ScoreboardDisplaySlot, String> {
|
||||
pub const fn get_display_slots(&self) -> &FxHashMap<ScoreboardDisplaySlot, String> {
|
||||
&self.display_slots
|
||||
}
|
||||
|
||||
@ -151,7 +151,7 @@ impl Scoreboard {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_scores(&self) -> &HashMap<String, HashMap<String, ScoreboardScore>> {
|
||||
pub const fn get_scores(&self) -> &FxHashMap<String, FxHashMap<String, ScoreboardScore>> {
|
||||
&self.scores
|
||||
}
|
||||
|
||||
@ -170,13 +170,13 @@ impl Scoreboard {
|
||||
pub fn get_scores_for_objective(
|
||||
&self,
|
||||
objective_name: &str,
|
||||
) -> Option<&HashMap<String, ScoreboardScore>> {
|
||||
) -> Option<&FxHashMap<String, ScoreboardScore>> {
|
||||
self.scores.get(objective_name)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_scores_for_entity(&self, entity_name: &str) -> HashMap<String, &ScoreboardScore> {
|
||||
let mut entity_scores = HashMap::new();
|
||||
pub fn get_scores_for_entity(&self, entity_name: &str) -> FxHashMap<String, &ScoreboardScore> {
|
||||
let mut entity_scores = FxHashMap::default();
|
||||
for (obj_name, obj_scores) in &self.scores {
|
||||
if let Some(score) = obj_scores.get(entity_name) {
|
||||
entity_scores.insert(obj_name.clone(), score);
|
||||
@ -186,7 +186,7 @@ impl Scoreboard {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_teams(&self) -> &HashMap<String, Team> {
|
||||
pub const fn get_teams(&self) -> &FxHashMap<String, Team> {
|
||||
&self.teams
|
||||
}
|
||||
|
||||
@ -859,9 +859,9 @@ pub struct BedrockObjective {
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct BedrockScoreboard {
|
||||
pub objectives: HashMap<String, BedrockObjective>,
|
||||
pub display_slots: HashMap<BedrockDisplaySlot, String>,
|
||||
pub scores: HashMap<(String, String), i32>,
|
||||
pub objectives: FxHashMap<String, BedrockObjective>,
|
||||
pub display_slots: FxHashMap<BedrockDisplaySlot, String>,
|
||||
pub scores: FxHashMap<(String, String), i32>,
|
||||
}
|
||||
|
||||
impl BedrockScoreboard {
|
||||
|
||||
@ -36,10 +36,18 @@ pub struct EntityType {
|
||||
pub category: MobCategory,
|
||||
/// Whether this entity can spawn far from the player (beyond normal spawn range).
|
||||
pub can_spawn_far_from_player: bool,
|
||||
/// Client tracking range in chunks.
|
||||
pub client_tracking_range: u32,
|
||||
/// Update interval in ticks.
|
||||
pub update_interval: u32,
|
||||
/// Whether movement deltas should be tracked.
|
||||
pub track_deltas: bool,
|
||||
/// Bounding box dimensions as `[width, height]` in blocks.
|
||||
pub dimension: [f32; 2],
|
||||
/// Eye height in blocks, used for line-of-sight calculations.
|
||||
pub eye_height: f32,
|
||||
/// Scale factor for spawn dimensions (e.g. slimes/magma cubes).
|
||||
pub spawn_dimensions_scale: f32,
|
||||
/// Spawn location restrictions for natural spawning.
|
||||
pub spawn_restriction: SpawnRestriction,
|
||||
}
|
||||
@ -173,6 +181,7 @@ impl ToTokens for NamedEntityType<'_> {
|
||||
|
||||
let dimension0 = entity.dimension[0];
|
||||
let dimension1 = entity.dimension[1];
|
||||
let spawn_dimensions_scale = entity.spawn_dimensions_scale;
|
||||
|
||||
let loot_table = if let Some(table) = &entity.loot_table {
|
||||
let table_tokens = table.to_token_stream();
|
||||
@ -182,6 +191,9 @@ impl ToTokens for NamedEntityType<'_> {
|
||||
};
|
||||
|
||||
let experience_reward = entity.experience_reward.unwrap_or(0);
|
||||
let client_tracking_range = entity.client_tracking_range;
|
||||
let update_interval = entity.update_interval;
|
||||
let track_deltas = entity.track_deltas;
|
||||
|
||||
tokens.extend(quote! {
|
||||
EntityType {
|
||||
@ -197,9 +209,13 @@ impl ToTokens for NamedEntityType<'_> {
|
||||
fire_immune: #fire_immune,
|
||||
category: &#spawn_category,
|
||||
can_spawn_far_from_player: #can_spawn_far_from_player,
|
||||
client_tracking_range: #client_tracking_range,
|
||||
update_interval: #update_interval,
|
||||
track_deltas: #track_deltas,
|
||||
loot_table: #loot_table,
|
||||
dimension: [#dimension0, #dimension1], // Correctly construct the array
|
||||
eye_height: #eye_height,
|
||||
spawn_dimensions_scale: #spawn_dimensions_scale,
|
||||
spawn_restriction: #spawn_restriction,
|
||||
resource_name: #name,
|
||||
}
|
||||
@ -250,6 +266,8 @@ pub fn build() -> TokenStream {
|
||||
use crate::sound::Sound;
|
||||
use pumpkin_util::loot_table::*;
|
||||
use pumpkin_util::HeightMap;
|
||||
use pumpkin_util::math::boundingbox::BoundingBox;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use std::hash::Hash;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -266,9 +284,13 @@ pub fn build() -> TokenStream {
|
||||
pub fire_immune: bool,
|
||||
pub category: &'static MobCategory,
|
||||
pub can_spawn_far_from_player: bool,
|
||||
pub client_tracking_range: u32,
|
||||
pub update_interval: u32,
|
||||
pub track_deltas: bool,
|
||||
pub loot_table: Option<LootTable>,
|
||||
pub dimension: [f32; 2],
|
||||
pub eye_height: f32,
|
||||
pub spawn_dimensions_scale: f32,
|
||||
pub spawn_restriction: SpawnRestriction,
|
||||
pub resource_name: &'static str,
|
||||
}
|
||||
@ -423,6 +445,31 @@ pub fn build() -> TokenStream {
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn width(&self) -> f32 {
|
||||
self.dimension[0]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn height(&self) -> f32 {
|
||||
self.dimension[1]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_spawn_bounding_box(&self, x: f64, y: f64, z: f64) -> BoundingBox {
|
||||
let half_width = f64::from(self.spawn_dimensions_scale * self.dimension[0] / 2.0);
|
||||
let height = f64::from(self.spawn_dimensions_scale * self.dimension[1]);
|
||||
BoundingBox::new(
|
||||
Vector3::new(x - half_width, y, z - half_width),
|
||||
Vector3::new(x + half_width, y + height, z + half_width),
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_spawn_aabb(&self, x: f64, y: f64, z: f64) -> BoundingBox {
|
||||
self.get_spawn_bounding_box(x, y, z)
|
||||
}
|
||||
}
|
||||
|
||||
impl IDSetContent for EntityType {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user