fix: slime spawns

This commit is contained in:
Alexander Medvedev 2026-08-29 15:58:32 +02:00
parent f6d0b4ab20
commit 09790fc093
53 changed files with 4229 additions and 1207 deletions

295
Cargo.lock generated
View File

@ -832,35 +832,6 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" 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]] [[package]]
name = "cpp_demangle" name = "cpp_demangle"
version = "0.5.1" version = "0.5.1"
@ -1444,15 +1415,6 @@ dependencies = [
"syn 3.0.4", "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]] [[package]]
name = "ecdsa" name = "ecdsa"
version = "0.16.9" version = "0.16.9"
@ -1824,8 +1786,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1835,9 +1799,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"rand_core 0.10.1", "rand_core 0.10.1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -2096,6 +2062,22 @@ dependencies = [
"want", "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]] [[package]]
name = "hyper-timeout" name = "hyper-timeout"
version = "0.5.2" version = "0.5.2"
@ -2115,13 +2097,16 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [ dependencies = [
"base64 0.22.1",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
"ipnet",
"libc", "libc",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio", "tokio",
@ -2466,12 +2451,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.14" version = "0.4.14"
@ -2487,6 +2466,12 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "lz4-java-wrc" name = "lz4-java-wrc"
version = "0.2.0" version = "0.2.0"
@ -3346,6 +3331,7 @@ dependencies = [
"pumpkin-world", "pumpkin-world",
"rand", "rand",
"rayon", "rayon",
"reqwest",
"rsa", "rsa",
"rustc-hash", "rustc-hash",
"rustc_version_runtime", "rustc_version_runtime",
@ -3364,7 +3350,6 @@ dependencies = [
"tracing", "tracing",
"tracing-serde-structured", "tracing-serde-structured",
"tracing-subscriber", "tracing-subscriber",
"ureq",
"uuid", "uuid",
"wasm-encoder 0.257.1", "wasm-encoder 0.257.1",
"wasmparser 0.257.1", "wasmparser 0.257.1",
@ -3455,6 +3440,7 @@ dependencies = [
"pumpkin-util", "pumpkin-util",
"pumpkin-world", "pumpkin-world",
"rand", "rand",
"rustc-hash",
"thiserror 2.0.20", "thiserror 2.0.20",
"tracing", "tracing",
] ]
@ -3502,12 +3488,12 @@ name = "pumpkin-plugin-utils"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"pumpkin-plugin-api", "pumpkin-plugin-api",
"reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tempfile", "tempfile",
"thiserror 2.0.20", "thiserror 2.0.20",
"tracing", "tracing",
"ureq",
] ]
[[package]] [[package]]
@ -3554,13 +3540,13 @@ dependencies = [
"pumpkin-codecs", "pumpkin-codecs",
"pumpkin-nbt", "pumpkin-nbt",
"quote", "quote",
"reqwest",
"rsa", "rsa",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.11.0", "sha2 0.11.0",
"syn 3.0.4", "syn 3.0.4",
"thiserror 2.0.20", "thiserror 2.0.20",
"ureq",
"uuid", "uuid",
] ]
@ -3605,6 +3591,62 @@ version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" 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]] [[package]]
name = "quinn-udp" name = "quinn-udp"
version = "0.6.1" version = "0.6.1"
@ -3668,6 +3710,15 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" 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]] [[package]]
name = "rayon" name = "rayon"
version = "1.12.0" version = "1.12.0"
@ -3775,6 +3826,47 @@ dependencies = [
"bytecheck", "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]] [[package]]
name = "rfc6979" name = "rfc6979"
version = "0.4.0" version = "0.4.0"
@ -4217,6 +4309,7 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [ dependencies = [
"web-time",
"zeroize", "zeroize",
] ]
@ -4262,6 +4355,12 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a252f5e20f038fe7b4ea53e073e65398d652c864cc162fc77c56c2f13717b888" checksum = "a252f5e20f038fe7b4ea53e073e65398d652c864cc162fc77c56c2f13717b888"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -4385,6 +4484,18 @@ dependencies = [
"serde_core", "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]] [[package]]
name = "serdect" name = "serdect"
version = "0.4.3" version = "0.4.3"
@ -4631,6 +4742,9 @@ name = "sync_wrapper"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]] [[package]]
name = "synstructure" name = "synstructure"
@ -4986,6 +5100,24 @@ dependencies = [
"tracing", "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]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.3" version = "0.3.3"
@ -5121,37 +5253,6 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 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]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@ -5164,12 +5265,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "utf8-zero"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]] [[package]]
name = "utf8_iter" name = "utf8_iter"
version = "1.0.4" version = "1.0.4"
@ -5245,6 +5340,16 @@ dependencies = [
"wasm-bindgen-shared", "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]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.127" version = "0.2.127"
@ -5656,6 +5761,26 @@ dependencies = [
"wasmtime", "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]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.9" version = "1.0.9"
@ -5678,7 +5803,7 @@ dependencies = [
"event-listener", "event-listener",
"futures", "futures",
"log", "log",
"quinn-udp", "quinn-udp 0.6.1",
"rtc", "rtc",
"tokio", "tokio",
] ]

View File

@ -207,7 +207,7 @@ time = { version = "0.3", default-features = false, features = ["std"] }
arc-swap = { version = "1.9", default-features = false } arc-swap = { version = "1.9", default-features = false }
tokio-util = { version = "0.7.19", default-features = false } tokio-util = { version = "0.7.19", default-features = false }
toml = { version = "1.1", default-features = false, features = ["parse", "display", "serde"] } 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"] } notify = { version = "8.2.0", default-features = false, features = ["macos_fsevent"] }
sysinfo = { version = "0.39", default-features = false, features = ["system"] } 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

View File

@ -25,6 +25,7 @@ pumpkin-util.workspace = true
rand.workspace = true rand.workspace = true
tracing.workspace = true tracing.workspace = true
thiserror.workspace = true thiserror.workspace = true
rustc-hash.workspace = true
[lints] [lints]
workspace = true workspace = true

View File

@ -58,7 +58,7 @@ pub mod stonecutter_screen_handler;
pub mod sync_handler; pub mod sync_handler;
pub mod window_property; pub mod window_property;
use std::collections::HashMap; use rustc_hash::FxHashMap;
pub use error::InventoryError; pub use error::InventoryError;
use pumpkin_data::data_component_impl::EquipmentSlot; 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. /// (head, chest, legs, feet, off-hand) used by the player screen handler.
/// ///
/// # Returns /// # 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] #[must_use]
pub fn build_equipment_slots() -> HashMap<usize, EquipmentSlot> { pub fn build_equipment_slots() -> FxHashMap<usize, EquipmentSlot> {
let mut equipment_slots = HashMap::new(); let mut equipment_slots = FxHashMap::default();
equipment_slots.insert( equipment_slots.insert(
EquipmentSlot::FEET.get_offset_entity_slot_id(PlayerInventory::MAIN_SIZE as i32) as usize, EquipmentSlot::FEET.get_offset_entity_slot_id(PlayerInventory::MAIN_SIZE as i32) as usize,
EquipmentSlot::FEET, EquipmentSlot::FEET,
@ -92,7 +92,6 @@ pub fn build_equipment_slots() -> HashMap<usize, EquipmentSlot> {
EquipmentSlot::HEAD, EquipmentSlot::HEAD,
); );
equipment_slots.insert(PlayerInventory::OFF_HAND_SLOT, EquipmentSlot::OFF_HAND);
equipment_slots.insert(PlayerInventory::OFF_HAND_SLOT, EquipmentSlot::OFF_HAND); equipment_slots.insert(PlayerInventory::OFF_HAND_SLOT, EquipmentSlot::OFF_HAND);
equipment_slots equipment_slots
} }

View File

@ -492,7 +492,6 @@ impl Slot for MerchantResultSlot {
mod tests { mod tests {
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::HashMap,
sync::atomic::{AtomicI32, AtomicUsize, Ordering}, sync::atomic::{AtomicI32, AtomicUsize, Ordering},
}; };
@ -627,7 +626,7 @@ mod tests {
( (
Arc::new(PlayerInventory::new( Arc::new(PlayerInventory::new(
Arc::new(Mutex::new(EntityEquipment::new())), Arc::new(Mutex::new(EntityEquipment::new())),
Arc::new(HashMap::new()), Arc::new(rustc_hash::FxHashMap::default()),
)), )),
Arc::new(SimpleInventory::new(3)), Arc::new(SimpleInventory::new(3)),
) )

View File

@ -16,8 +16,8 @@ use pumpkin_data::item_stack::ItemStack;
use pumpkin_protocol::java::client::play::CSetPlayerInventory; use pumpkin_protocol::java::client::play::CSetPlayerInventory;
use pumpkin_util::Hand; use pumpkin_util::Hand;
use pumpkin_world::inventory::{Clearable, Inventory}; use pumpkin_world::inventory::{Clearable, Inventory};
use rustc_hash::FxHashMap;
use std::any::Any; use std::any::Any;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
@ -35,7 +35,7 @@ pub struct PlayerInventory {
/// Mapping of slot indices to equipment slot types. /// Mapping of slot indices to equipment slot types.
/// ///
/// Used to identify which slots correspond to armor and off-hand equipment. /// 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). /// The currently selected hotbar slot index (0-8).
pub selected_slot: AtomicU8, pub selected_slot: AtomicU8,
/// The entity equipment storage for armor and off-hand items. /// The entity equipment storage for armor and off-hand items.
@ -60,7 +60,7 @@ impl PlayerInventory {
// TODO: Add inventory load from nbt // TODO: Add inventory load from nbt
pub fn new( pub fn new(
entity_equipment: Arc<Mutex<EntityEquipment>>, entity_equipment: Arc<Mutex<EntityEquipment>>,
equipment_slots: Arc<HashMap<usize, EquipmentSlot>>, equipment_slots: Arc<FxHashMap<usize, EquipmentSlot>>,
) -> Self { ) -> Self {
Self { Self {
main_inventory: RwLock::new(std::array::from_fn(|_| ItemStack::EMPTY.clone())), main_inventory: RwLock::new(std::array::from_fn(|_| ItemStack::EMPTY.clone())),

View File

@ -11,7 +11,7 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
ureq = { workspace = true, features = ["json"] } reqwest = { workspace = true, features = ["blocking", "json"] }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }

View File

@ -18,7 +18,7 @@ pub enum HttpError {
/// Helper client for querying Pumpkin marketplace REST APIs. /// Helper client for querying Pumpkin marketplace REST APIs.
pub struct HttpClient { pub struct HttpClient {
user_agent: String, client: reqwest::blocking::Client,
} }
impl Default for HttpClient { impl Default for HttpClient {
@ -31,9 +31,11 @@ impl HttpClient {
/// Creates a new HTTP client with the specified User-Agent header. /// Creates a new HTTP client with the specified User-Agent header.
#[must_use] #[must_use]
pub fn new(user_agent: &str) -> Self { pub fn new(user_agent: &str) -> Self {
Self { let client = reqwest::blocking::Client::builder()
user_agent: user_agent.to_string(), .user_agent(user_agent)
} .build()
.unwrap_or_default();
Self { client }
} }
/// Performs an HTTP GET request and returns the response body as a string. /// 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. /// Returns `HttpError` if the request fails or returns a non-2xx status code.
pub fn get(&self, url: &str) -> Result<String, HttpError> { pub fn get(&self, url: &str) -> Result<String, HttpError> {
let mut response = ureq::get(url) let response = self
.header("User-Agent", &self.user_agent) .client
.get(url)
.header("Accept", "application/json") .header("Accept", "application/json")
.call() .send()
.map_err(|e| HttpError::RequestFailed(e.to_string()))?; .map_err(|e| HttpError::RequestFailed(e.to_string()))?;
let status = response.status().as_u16(); let status = response.status().as_u16();
if status < 200 || status >= 300 { if status < 200 || status >= 300 {
let body = response let body = response
.body_mut() .text()
.read_to_string()
.unwrap_or_else(|_| "Unknown error".to_string()); .unwrap_or_else(|_| "Unknown error".to_string());
return Err(HttpError::BadStatus(status, body)); return Err(HttpError::BadStatus(status, body));
} }
response response
.body_mut() .text()
.read_to_string()
.map_err(|e| HttpError::BodyRead(e.to_string())) .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. /// 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> { pub fn post_json(&self, url: &str, json_payload: &str) -> Result<String, HttpError> {
let mut response = ureq::post(url) let response = self
.header("User-Agent", &self.user_agent) .client
.post(url)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Accept", "application/json") .header("Accept", "application/json")
.send(json_payload) .body(json_payload.to_string())
.send()
.map_err(|e| HttpError::RequestFailed(e.to_string()))?; .map_err(|e| HttpError::RequestFailed(e.to_string()))?;
let status = response.status().as_u16(); let status = response.status().as_u16();
if status < 200 || status >= 300 { if status < 200 || status >= 300 {
let body = response let body = response
.body_mut() .text()
.read_to_string()
.unwrap_or_else(|_| "Unknown error".to_string()); .unwrap_or_else(|_| "Unknown error".to_string());
return Err(HttpError::BadStatus(status, body)); return Err(HttpError::BadStatus(status, body));
} }
response response
.body_mut() .text()
.read_to_string()
.map_err(|e| HttpError::BodyRead(e.to_string())) .map_err(|e| HttpError::BodyRead(e.to_string()))
} }
} }

View File

@ -30,7 +30,7 @@ ecdsa.workspace = true
rsa.workspace = true rsa.workspace = true
sha2.workspace = true sha2.workspace = true
crypto-bigint.workspace = true crypto-bigint.workspace = true
ureq.workspace = true reqwest.workspace = true
[dev-dependencies] [dev-dependencies]
criterion = { workspace = true, features = ["html_reports"] } criterion = { workspace = true, features = ["html_reports"] }

View File

@ -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"; "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. /// 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>, discovery_url: Option<&str>,
connect_timeout_ms: u32, connect_timeout_ms: u32,
read_timeout_ms: u32, read_timeout_ms: u32,
) -> Result<(String, Jwks), AuthError> { ) -> Result<(String, Jwks), AuthError> {
let url = discovery_url.unwrap_or(OIDC_DISCOVERY_URL); let url = discovery_url.unwrap_or(OIDC_DISCOVERY_URL);
let config = ureq::Agent::config_builder() let client = reqwest::Client::builder()
.timeout_connect(Some(std::time::Duration::from_millis( .connect_timeout(std::time::Duration::from_millis(connect_timeout_ms as u64))
connect_timeout_ms as u64, .timeout(std::time::Duration::from_millis(read_timeout_ms as u64))
))) .build()
.timeout_recv_response(Some(std::time::Duration::from_millis( .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?;
read_timeout_ms as u64,
)))
.build();
let agent: ureq::Agent = config.into();
let discovery: Value = agent let discovery: Value = client
.get(url) .get(url)
.call() .send()
.await
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))? .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
.body_mut() .json()
.read_to_string() .await
.map_err(|e| AuthError::PublicKeyBuild(format!("Failed to read response: {e}"))) .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?;
.and_then(|s| {
serde_json::from_str(&s).map_err(|e| AuthError::PublicKeyBuild(e.to_string()))
})?;
let service_uri = discovery let service_uri = discovery
.get("result") .get("result")
@ -281,16 +275,14 @@ pub fn fetch_oidc_jwks(
.ok_or_else(|| AuthError::PublicKeyBuild("Discovery missing serviceUri".into()))?; .ok_or_else(|| AuthError::PublicKeyBuild("Discovery missing serviceUri".into()))?;
let openid_config_url = format!("{service_uri}/.well-known/openid-configuration"); 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) .get(&openid_config_url)
.call() .send()
.await
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))? .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
.body_mut() .json()
.read_to_string() .await
.map_err(|e| AuthError::PublicKeyBuild(format!("Failed to read response: {e}"))) .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?;
.and_then(|s| {
serde_json::from_str(&s).map_err(|e| AuthError::PublicKeyBuild(e.to_string()))
})?;
let jwks_uri = openid_config let jwks_uri = openid_config
.get("jwks_uri") .get("jwks_uri")
@ -303,16 +295,14 @@ pub fn fetch_oidc_jwks(
.ok_or_else(|| AuthError::PublicKeyBuild("OpenID config missing issuer".into()))? .ok_or_else(|| AuthError::PublicKeyBuild("OpenID config missing issuer".into()))?
.to_string(); .to_string();
let jwks: Jwks = agent let jwks: Jwks = client
.get(jwks_uri) .get(jwks_uri)
.call() .send()
.await
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))? .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
.body_mut() .json()
.read_to_string() .await
.map_err(|e| AuthError::PublicKeyBuild(format!("Failed to read response: {e}"))) .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?;
.and_then(|s| {
serde_json::from_str(&s).map_err(|e| AuthError::PublicKeyBuild(e.to_string()))
})?;
Ok((issuer, jwks)) Ok((issuer, jwks))
} }

View File

@ -1,4 +1,4 @@
use std::collections::HashMap; use rustc_hash::FxHashMap;
use std::io::{ErrorKind, Read}; use std::io::{ErrorKind, Read};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::path::PathBuf; use std::path::PathBuf;
@ -141,15 +141,15 @@ impl ChunkBitmap {
// Repeated: u8 key_len, key bytes, u32 value // Repeated: u8 key_len, key bytes, u32 value
// Terminated by a single 0x00 byte. // Terminated by a single 0x00 byte.
struct NbtFeatures(HashMap<String, u32>); struct NbtFeatures(FxHashMap<String, u32>);
impl NbtFeatures { impl NbtFeatures {
fn empty() -> Self { fn empty() -> Self {
Self(HashMap::new()) Self(FxHashMap::default())
} }
fn from_bytes(buf: &mut impl Buf) -> Result<Self, ChunkReadingError> { fn from_bytes(buf: &mut impl Buf) -> Result<Self, ChunkReadingError> {
let mut map = HashMap::new(); let mut map = FxHashMap::default();
loop { loop {
if !buf.has_remaining() { if !buf.has_remaining() {
return Err(ChunkReadingError::IoError(std::io::Error::from( return Err(ChunkReadingError::IoError(std::io::Error::from(

View File

@ -845,28 +845,31 @@ impl LightContainer {
matches!(self, Self::Empty(_)) matches!(self, Self::Empty(_))
} }
#[inline]
const fn index(x: usize, y: usize, z: usize) -> usize { const fn index(x: usize, y: usize, z: usize) -> usize {
y * 16 * 16 + z * 16 + x y * 16 * 16 + z * 16 + x
} }
#[inline]
#[must_use] #[must_use]
pub fn get(&self, x: usize, y: usize, z: usize) -> u8 { pub fn get(&self, x: usize, y: usize, z: usize) -> u8 {
match self { match self {
Self::Full(data) => { Self::Full(data) => {
let index = Self::index(x, y, z); 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, Self::Empty(default) => *default,
} }
} }
#[inline]
pub fn set(&mut self, x: usize, y: usize, z: usize, value: u8) { pub fn set(&mut self, x: usize, y: usize, z: usize, value: u8) {
match self { match self {
Self::Full(data) => { Self::Full(data) => {
let index = Self::index(x, y, z); let index = Self::index(x, y, z);
let mask = 0x0F << (4 * (index & 1)); let shift = 4 * (index & 1);
data[index >> 1] &= !mask; let mask = 0x0F << shift;
data[index >> 1] |= value << (4 * (index & 1)); data[index >> 1] = (data[index >> 1] & !mask) | (value << shift);
} }
Self::Empty(default) => { Self::Empty(default) => {
if value != *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) { pub fn fill(&mut self, value: u8) {
*self = Self::new_filled(value); *self = Self::new_filled(value);
} }

View File

@ -242,41 +242,82 @@ impl GenerationSchedule {
chunk_map: &HashMap<ChunkPos, ChunkHolder>, chunk_map: &HashMap<ChunkPos, ChunkHolder>,
last_level: &ChunkLevel, last_level: &ChunkLevel,
last_high_priority: &[ChunkPos], last_high_priority: &[ChunkPos],
waiting_for_chunks: &HashSetType<NodeKey>, _waiting_for_chunks: &HashSetType<NodeKey>,
) -> usize { ) -> usize {
debug_assert!(queue.is_empty()); debug_assert!(queue.is_empty());
let mut to_drop = Vec::new();
let mut ready = Vec::new(); let mut ready = Vec::new();
for (key, node) in &mut graph.nodes {
if node.stage == StagedChunkEnum::None for (key, node) in &graph.nodes {
|| node.in_degree != 0 if node.stage == StagedChunkEnum::None || node.in_flight {
|| node.in_queue
|| node.in_flight
|| waiting_for_chunks.contains(&key)
{
continue; continue;
} }
let Some(holder) = chunk_map.get(&node.pos) else { let Some(holder) = chunk_map.get(&node.pos) else {
to_drop.push(key);
continue; 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; continue;
} }
ready.push((key, node.pos, node.stage));
if node.in_degree == 0 && !node.in_queue {
ready.push((key, node.pos, node.stage));
}
} }
for (key, pos, stage) in &ready { for key in to_drop {
let Some(node) = graph.nodes.get_mut(*key) else { let Some(old) = graph.nodes.remove(key) else {
continue; continue;
}; };
node.in_queue = true; let mut edge = old.edge;
queue.push(TaskHeapNode( while !edge.is_null() {
Self::calc_priority(last_level, last_high_priority, *pos, *stage), let Some(cur) = graph.edges.remove(edge) else {
*key, 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;
}
} }
ready.len() 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 /// Ensure that the dependency chain for `req_stage` exists on `holder` (for chunk at
@ -296,7 +337,7 @@ impl GenerationSchedule {
chunk_pos: ChunkPos, chunk_pos: ChunkPos,
holder: &mut ChunkHolder, holder: &mut ChunkHolder,
req_stage: StagedChunkEnum, req_stage: StagedChunkEnum,
) { ) -> [bool; StagedChunkEnum::COUNT] {
// Insert occupied_by edge head // Insert occupied_by edge head
holder.occupied_by = graph.edges.insert(crate::chunk_system::dag::Edge::new( holder.occupied_by = graph.edges.insert(crate::chunk_system::dag::Edge::new(
dependency_task, dependency_task,
@ -317,6 +358,7 @@ impl GenerationSchedule {
// Effective target is the max of what the player wants and what dependencies need. // 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 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. // 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 // 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 empty = StagedChunkEnum::Empty as usize;
let start = (holder.current_stage as usize + 1).max(empty); let start = (holder.current_stage as usize + 1).max(empty);
let end = effective_target as u8 as usize; 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() { for (i, flag) in newly_created[start..=end].iter_mut().enumerate() {
let stage_i = start + i; 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. // 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). // Do NOT add an edge here: tasks[req_stage] is null (completed and dropped).
if holder.current_stage >= req_stage { if holder.current_stage >= req_stage {
return; return newly_created;
} }
// Wire req_stage task → dependency_task so dependency_task can't run until // 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" "holder.tasks[req_stage] must not be null before adding edge"
); );
graph.add_edge(ano_task, dependency_task); graph.add_edge(ano_task, dependency_task);
newly_created
} }
/// Check if any tasks parked in `waiting_for_chunks` now have all their neighbor /// 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 now_ready: Vec<NodeKey> = Vec::new();
let mut stale: Vec<NodeKey> = Vec::new();
self.waiting_for_chunks.retain(|&node_key| { self.waiting_for_chunks.retain(|&node_key| {
let Some(node) = self.graph.nodes.get(node_key) else { let Some(node) = self.graph.nodes.get(node_key) else {
return false; // node was dropped, discard silently 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 read_radius = node.stage.get_read_radius();
let pos = node.pos; let pos = node.pos;
let all_ready = (-read_radius..=read_radius).all(|dx| { 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 { for node_key in now_ready {
if let Some(n) = self.graph.nodes.get_mut(node_key) if let Some(n) = self.graph.nodes.get_mut(node_key)
&& n.in_degree == 0 && n.in_degree == 0
@ -462,6 +525,9 @@ impl GenerationSchedule {
let Some(new_level) = new_data.0 else { let Some(new_level) = new_data.0 else {
return true; 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 { for (pos, (old_stage, new_stage)) in new_level.0 {
debug_assert_ne!(old_stage, new_stage); debug_assert_ne!(old_stage, new_stage);
debug_assert_eq!( debug_assert_eq!(
@ -537,7 +603,7 @@ impl GenerationSchedule {
let new_pos = pos.add_raw(dx, dz); let new_pos = pos.add_raw(dx, dz);
let req_stage = dependency[dx.abs().max(dz.abs()) as usize]; let req_stage = dependency[dx.abs().max(dz.abs()) as usize];
if new_pos == pos { if new_pos == pos {
Self::ensure_dependency_chain( let newly_created = Self::ensure_dependency_chain(
&mut self.graph, &mut self.graph,
&mut self.queue, &mut self.queue,
&self.last_level, &self.last_level,
@ -547,20 +613,33 @@ impl GenerationSchedule {
&mut holder, &mut holder,
req_stage, 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; continue;
} }
let ano_chunk = self.chunk_map.entry(new_pos).or_default(); worklist.push_back((new_pos, req_stage, task));
Self::ensure_dependency_chain(
&mut self.graph,
&mut self.queue,
&self.last_level,
&self.last_high_priority,
task,
new_pos,
ano_chunk,
req_stage,
);
} }
} }
} }
@ -573,6 +652,40 @@ impl GenerationSchedule {
} }
self.chunk_map.insert(pos, holder); 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.last_level = new_level.1;
self.queue_dirty = true; self.queue_dirty = true;
true true
@ -852,20 +965,16 @@ impl GenerationSchedule {
debug_assert!(node.in_degree >= 1); debug_assert!(node.in_degree >= 1);
node.in_degree -= 1; node.in_degree -= 1;
if node.in_degree == 0 && !node.in_queue { if node.in_degree == 0 && !node.in_queue {
// Don't queue if parked in waiting_for_chunks — check_waiting_tasks() self.queue.push(TaskHeapNode(
// will re-queue it once chunk data arrives. Self::calc_priority(
if !self.waiting_for_chunks.contains(&cur.to) { &self.last_level,
self.queue.push(TaskHeapNode( &self.last_high_priority,
Self::calc_priority( node.pos,
&self.last_level, node.stage,
&self.last_high_priority, ),
node.pos, cur.to,
node.stage, ));
), node.in_queue = true;
cur.to,
));
node.in_queue = true;
}
} }
} }
edge = cur.next; 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); holder.chunk = Some(chunk);
self.chunk_map.insert(pos, holder); self.chunk_map.insert(pos, holder);
@ -1021,15 +1121,6 @@ impl GenerationSchedule {
} }
holder.occupied = NodeKey::null(); 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); self.chunk_map.insert(new_pos, holder);
} }
Chunk::Proto(chunk) => { 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.occupied = NodeKey::null();
holder.chunk = Some(Chunk::Proto(chunk)); holder.chunk = Some(Chunk::Proto(chunk));
self.chunk_map.insert(new_pos, holder); self.chunk_map.insert(new_pos, holder);

View File

@ -4,34 +4,73 @@ use crate::generation::height_limit::HeightLimitView;
use crate::generation::proto_chunk::GenerationCache; use crate::generation::proto_chunk::GenerationCache;
use crate::lighting::storage::{get_block_light, get_sky_light, set_block_light, set_sky_light}; use crate::lighting::storage::{get_block_light, get_sky_light, set_block_light, set_sky_light};
use pumpkin_config::lighting::LightingEngineConfig; use pumpkin_config::lighting::LightingEngineConfig;
use pumpkin_data::BlockDirection; use pumpkin_data::{BlockDirection, BlockStateId};
use pumpkin_util::HeightMap; use pumpkin_util::HeightMap;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use std::collections::VecDeque; use std::collections::VecDeque;
//use std::time::Instant;
// These are hit on every neighbour of every propagated block, so the hash use crate::ProtoChunk;
// 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>;
/// Trait to unify Block and Sky light logic
pub trait LightProvider { pub trait LightProvider {
fn get_light(cache: &Cache, pos: BlockPos) -> u8; fn get_light(cache: &Cache, pos: BlockPos) -> u8;
fn set_light(cache: &mut Cache, pos: BlockPos, level: 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; fn propagate_level(current_level: u8, opacity: u8, dir: BlockDirection) -> u8;
} }
pub struct BlockLightProvider; pub struct BlockLightProvider;
impl LightProvider for BlockLightProvider { impl LightProvider for BlockLightProvider {
#[inline]
fn get_light(cache: &Cache, pos: BlockPos) -> u8 { fn get_light(cache: &Cache, pos: BlockPos) -> u8 {
get_block_light(cache, pos) get_block_light(cache, pos)
} }
#[inline]
fn set_light(cache: &mut Cache, pos: BlockPos, level: u8) { fn set_light(cache: &mut Cache, pos: BlockPos, level: u8) {
set_block_light(cache, pos, level); 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 { fn propagate_level(current_level: u8, opacity: u8, _dir: BlockDirection) -> u8 {
current_level.saturating_sub(opacity.max(1)) current_level.saturating_sub(opacity.max(1))
} }
@ -39,12 +78,42 @@ impl LightProvider for BlockLightProvider {
pub struct SkyLightProvider; pub struct SkyLightProvider;
impl LightProvider for SkyLightProvider { impl LightProvider for SkyLightProvider {
#[inline]
fn get_light(cache: &Cache, pos: BlockPos) -> u8 { fn get_light(cache: &Cache, pos: BlockPos) -> u8 {
get_sky_light(cache, pos) get_sky_light(cache, pos)
} }
#[inline]
fn set_light(cache: &mut Cache, pos: BlockPos, level: u8) { fn set_light(cache: &mut Cache, pos: BlockPos, level: u8) {
set_sky_light(cache, pos, level); 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 { fn propagate_level(current_level: u8, opacity: u8, dir: BlockDirection) -> u8 {
if current_level == 15 && dir == BlockDirection::Down && opacity == 0 { if current_level == 15 && dir == BlockDirection::Down && opacity == 0 {
return 15; return 15;
@ -57,12 +126,114 @@ impl LightProvider for SkyLightProvider {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct PropagationEntry { pub struct PropagationEntry {
pos: BlockPos, 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 struct LightPropagator<P: LightProvider> {
pub(crate) queue: VecDeque<PropagationEntry>, pub(crate) queue: VecDeque<PropagationEntry>,
pub(crate) visited: FastHashSet<BlockPos>, pub(crate) visited: VisitedBitSet,
pub(crate) decrease_queue: VecDeque<(BlockPos, u8)>, pub(crate) decrease_queue: VecDeque<(BlockPos, u8)>,
_marker: std::marker::PhantomData<P>, _marker: std::marker::PhantomData<P>,
} }
@ -71,8 +242,8 @@ impl<P: LightProvider> LightPropagator<P> {
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
queue: VecDeque::with_capacity(4096), queue: VecDeque::with_capacity(8192),
visited: FastHashSet::default(), visited: VisitedBitSet::new(),
decrease_queue: VecDeque::new(), decrease_queue: VecDeque::new(),
_marker: std::marker::PhantomData, _marker: std::marker::PhantomData,
} }
@ -84,19 +255,21 @@ impl<P: LightProvider> LightPropagator<P> {
self.decrease_queue.clear(); 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) { pub fn propagate(&mut self, cache: &mut Cache) {
// Cache metadata for bounds checking
let cache_x = cache.x; let cache_x = cache.x;
let cache_z = cache.z; let cache_z = cache.z;
let cache_size = cache.size; let cache_size = cache.size;
let min_y = cache.bottom_y() as i32; let min_y = cache.bottom_y() as i32;
let max_y = min_y + cache.height() 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() { while let Some(entry) = self.queue.pop_front() {
let pos = entry.pos; let pos = entry.pos;
@ -106,7 +279,6 @@ impl<P: LightProvider> LightPropagator<P> {
} }
for dir in BlockDirection::all() { for dir in BlockDirection::all() {
// Skip the direction we came from (if specified)
if let Some(skip_dir) = entry.skip_direction if let Some(skip_dir) = entry.skip_direction
&& dir == skip_dir && dir == skip_dir
{ {
@ -114,36 +286,78 @@ impl<P: LightProvider> LightPropagator<P> {
} }
let neighbor_pos = pos.offset(dir.to_offset()); 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.is_visited(nx, ny, nz) {
if self.visited.contains(&neighbor_pos) {
continue; continue;
} }
// Skip neighbor if it's outside world bounds if ny < min_y || ny >= max_y {
if neighbor_pos.0.y < min_y || neighbor_pos.0.y >= max_y {
continue; continue;
} }
let (cx, _rel) = neighbor_pos.chunk_and_chunk_relative_position(); let rel_x = (nx >> 4) - cache_x;
let rel_x = cx.x - cache_x; let rel_z = (nz >> 4) - cache_z;
let rel_z = cx.y - cache_z;
if rel_x < 0 || rel_x >= cache_size || rel_z < 0 || rel_z >= cache_size { if rel_x < 0 || rel_x >= cache_size || rel_z < 0 || rel_z >= cache_size {
continue; continue;
} }
// Get block opacity let chunk_idx = (rel_x * cache_size + rel_z) as usize;
let state = cache.get_block_state(&neighbor_pos.0); let local_x = (nx & 15) as usize;
let opacity = state.to_state().opacity; 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 new_level = P::propagate_level(current_light, opacity, dir);
let neighbor_light = P::get_light(cache, neighbor_pos);
if new_level > neighbor_light { if new_level > neighbor_light {
P::set_light(cache, neighbor_pos, new_level); 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.test_and_set(nx, ny, nz) {
if new_level > 1 && self.visited.insert(neighbor_pos) {
self.queue.push_back(PropagationEntry { self.queue.push_back(PropagationEntry {
pos: neighbor_pos, pos: neighbor_pos,
skip_direction: Some(dir.opposite()), skip_direction: Some(dir.opposite()),
@ -154,55 +368,51 @@ impl<P: LightProvider> LightPropagator<P> {
} }
} }
/// Handle light removal
pub fn process_decrease_queue(&mut self, cache: &mut Cache) { pub fn process_decrease_queue(&mut self, cache: &mut Cache) {
{ let cache_x = cache.x;
// Cache metadata for bounds checking let cache_z = cache.z;
let cache_x = cache.x; let cache_size = cache.size;
let cache_z = cache.z;
let cache_size = cache.size;
while let Some((pos, old_val)) = self.decrease_queue.pop_front() { while let Some((pos, old_val)) = self.decrease_queue.pop_front() {
for dir in BlockDirection::all() { for dir in BlockDirection::all() {
let neighbor_pos = pos.offset(dir.to_offset()); let neighbor_pos = pos.offset(dir.to_offset());
// Bounds check let (cx, _rel) = neighbor_pos.chunk_and_chunk_relative_position();
let (cx, _rel) = neighbor_pos.chunk_and_chunk_relative_position(); let rel_x = cx.x - cache_x;
let rel_x = cx.x - cache_x; let rel_z = cx.y - cache_z;
let rel_z = cx.y - cache_z;
if rel_x < 0 || rel_x >= cache_size || rel_z < 0 || rel_z >= cache_size { if rel_x < 0 || rel_x >= cache_size || rel_z < 0 || rel_z >= cache_size {
continue; continue;
} }
let neighbor_light = P::get_light(cache, neighbor_pos); let neighbor_light = P::get_light(cache, neighbor_pos);
if neighbor_light == 0 { if neighbor_light == 0 {
continue; continue;
} }
let state = cache.get_block_state(&neighbor_pos.0); let state = cache.get_block_state(&neighbor_pos.0);
let opacity = state.to_state().opacity; let opacity = state.to_state().opacity;
let predicted = P::propagate_level(old_val, opacity, dir); let predicted = P::propagate_level(old_val, opacity, dir);
if neighbor_light == predicted || neighbor_light < old_val { if neighbor_light == predicted || neighbor_light < old_val {
// Darken P::set_light(cache, neighbor_pos, 0);
P::set_light(cache, neighbor_pos, 0); self.decrease_queue
self.decrease_queue .push_back((neighbor_pos, neighbor_light));
.push_back((neighbor_pos, neighbor_light)); } else if neighbor_light >= old_val {
} else if neighbor_light >= old_val { let nx = neighbor_pos.0.x;
// Re-illuminate from this bright neighbor let ny = neighbor_pos.0.y;
self.queue.push_back(PropagationEntry { let nz = neighbor_pos.0.z;
pos: neighbor_pos, self.queue.push_back(PropagationEntry {
skip_direction: None, 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) { pub fn propagate_light(&mut self, cache: &mut Cache) {
self.clear(); self.clear();
//let scan_start = Instant::now();
let min_y = cache.bottom_y() as i32; let min_y = cache.bottom_y() as i32;
let max_y = min_y + cache.height() as i32; let max_y = min_y + cache.height() as i32;
let center_x = cache.x + (cache.size / 2); let center_x = cache.x + (cache.size / 2);
@ -231,33 +439,92 @@ impl BlockLightPropagator {
let end_x = start_x + 18; let end_x = start_x + 18;
let end_z = start_z + 18; let end_z = start_z + 18;
for y in min_y..max_y { let min_x = cache.x * 16;
for z in start_z..end_z { let min_z = cache.z * 16;
for x in start_x..end_x { let size_x = (cache.size * 16) as usize;
let pos_vec = Vector3::new(x, y, z); let size_z = (cache.size * 16) as usize;
let state = cache.get_block_state(&pos_vec); let size_y = (max_y - min_y) as usize;
let emission = state.to_state().luminance; self.visited
if emission > 0 { .ensure_capacity(min_x, min_y, min_z, size_x, size_y, size_z);
let pos = BlockPos(pos_vec);
set_block_light(cache, pos, emission); for z in start_z..end_z {
if self.visited.insert(pos) { let rel_z = (z >> 4) - cache.z;
// Block light propagates in all directions let local_z = (z & 15) as usize;
self.queue.push_back(PropagationEntry {
pos, for x in start_x..end_x {
skip_direction: None, 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 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,
});
}
}
} }
} }
} }
} }
} }
//let scan_elapsed = scan_start.elapsed();
//let propagate_start = Instant::now();
self.propagate(cache); 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) { pub fn convert_light(&mut self, cache: &mut Cache) {
self.clear(); self.clear();
//let scan_start = Instant::now();
let center_x = cache.x + (cache.size / 2); let center_x = cache.x + (cache.size / 2);
let center_z = cache.z + (cache.size / 2); let center_z = cache.z + (cache.size / 2);
let start_x = center_x * 16 - 1; let start_x = center_x * 16 - 1;
@ -278,25 +543,29 @@ impl SkyLightPropagator {
let bottom_y = cache.bottom_y() as i32; let bottom_y = cache.bottom_y() as i32;
let max_y = bottom_y + cache.height() as i32; let max_y = bottom_y + cache.height() as i32;
// Pre-allocate with exact size needed let min_x = cache.x * 16;
let capacity = ((end_x - start_x) * (end_z - start_z)) as usize; let min_z = cache.z * 16;
let mut surface_heights = let size_x = (cache.size * 16) as usize;
FastHashMap::with_capacity_and_hasher(capacity, rustc_hash::FxBuildHasher); 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 { for z in start_z..end_z {
let chunk_z = z >> 4; let chunk_z = z >> 4;
let local_z = (z & 15) as usize; let local_z = (z & 15) as usize;
let lz = (z - start_z) as usize;
for x in start_x..end_x { for x in start_x..end_x {
let chunk_x = x >> 4; let chunk_x = x >> 4;
let local_x = (x & 15) as usize; 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); 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_x = chunk_x - cache.x;
let rel_z = chunk_z - cache.z; let rel_z = chunk_z - cache.z;
@ -306,89 +575,123 @@ impl SkyLightPropagator {
let chunk_idx = (rel_x * cache.size + rel_z) as usize; let chunk_idx = (rel_x * cache.size + rel_z) as usize;
// Fill everything above heightmap with 15 immediately match &mut cache.chunks[chunk_idx] {
for y in (top_y + 1)..max_y { Chunk::Proto(c) => {
let section_idx = ((y - bottom_y) >> 4) as usize; let top_local_y = (top_y + 1 - bottom_y).max(0) as usize;
let local_y = (y & 15) as usize; let top_sec = top_local_y >> 4;
let top_rem = top_local_y & 15;
// Direct array access - skip all function call overhead if top_sec < c.light.sky_light.len() {
match &mut cache.chunks[chunk_idx] { c.light.sky_light[top_sec]
Chunk::Proto(c) => { .set_column_y_range(local_x, local_z, top_rem, 16, 15);
if section_idx < c.light.sky_light.len() { for sec in (top_sec + 1)..c.light.sky_light.len() {
c.light.sky_light[section_idx].set(local_x, local_y, local_z, 15); c.light.sky_light[sec]
.set_column_y_range(local_x, local_z, 0, 16, 15);
} }
} }
Chunk::Level(c) => {
let mut light_engine = c let mut light: i32 = 15;
.light_engine for y in (bottom_y..=top_y).rev() {
.lock() let local_y_proto = y - bottom_y;
.unwrap_or_else(std::sync::PoisonError::into_inner); 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, light_val);
}
if light <= 0 {
break;
}
}
}
Chunk::Level(c) => {
let mut light_engine = c
.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() { if section_idx < light_engine.sky_light.len() {
light_engine.sky_light[section_idx] light_engine.sky_light[section_idx]
.set(local_x, local_y, local_z, 15); .set(local_x, local_y, local_z, 15);
} }
} }
}
}
// Only iterate from top_y DOWN - not from max_y let mut light: i32 = 15;
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;
for y in (bottom_y..=top_y).rev() { let state_id = c
let section_idx = ((y - bottom_y) >> 4) as usize; .section
let local_y = (y & 15) as usize; .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
};
// Get block opacity light = light.saturating_sub(opacity);
let opacity = { let light_val = if light <= 0 { 0 } else { light as u8 };
let pos_vec = Vector3::new(x, y, z);
let state = cache.get_block_state(&pos_vec);
state.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() { if section_idx < light_engine.sky_light.len() {
light_engine.sky_light[section_idx] light_engine.sky_light[section_idx]
.set(local_x, local_y, local_z, light_val); .set(local_x, local_y, local_z, light_val);
} }
}
}
// Early exit when light hits 0 if light <= 0 {
if light <= 0 { break;
break; }
}
} }
} }
} }
} }
// Enqueue horizontal propagation
for z in start_z..end_z { for z in start_z..end_z {
let lz = (z - start_z) as usize;
for x in start_x..end_x { 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 north_top = if lz > 0 {
let south_top = surface_heights.get(&(x, z + 1)).copied().unwrap_or(top_y); surface_heights[lx * 18 + (lz - 1)]
let west_top = surface_heights.get(&(x - 1, z)).copied().unwrap_or(top_y); } else {
let east_top = surface_heights.get(&(x + 1, z)).copied().unwrap_or(top_y); 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 let max_check_y = top_y
.max(north_top) .max(north_top)
.max(south_top) .max(south_top)
@ -399,7 +702,6 @@ impl SkyLightPropagator {
let pos = BlockPos(Vector3::new(x, y, z)); let pos = BlockPos(Vector3::new(x, y, z));
let light = get_sky_light(cache, pos); 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 light == 0 {
if y <= top_y { if y <= top_y {
break; break;
@ -411,7 +713,7 @@ impl SkyLightPropagator {
let below_neighbor = let below_neighbor =
y < north_top || y < south_top || y < west_top || y < east_top; 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); let skip_dir = (y >= top_y).then_some(BlockDirection::Up);
self.queue.push_back(PropagationEntry { self.queue.push_back(PropagationEntry {
@ -423,13 +725,7 @@ impl SkyLightPropagator {
} }
} }
//let propagate_start = Instant::now();
self.propagate(cache); 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, old_luminance: u8,
new_luminance: u8, new_luminance: u8,
) { ) {
// Decrease Logic
if old_luminance > new_luminance { if old_luminance > new_luminance {
let current_light = get_block_light(cache, pos); let current_light = get_block_light(cache, pos);
if current_light > 0 { if current_light > 0 {
@ -485,10 +780,13 @@ impl LightEngine {
} }
} }
// Increase Logic
if new_luminance > 0 { if new_luminance > 0 {
set_block_light(cache, pos, new_luminance); 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 { self.block_light.queue.push_back(PropagationEntry {
pos, pos,
skip_direction: None, skip_direction: None,

View File

@ -3,6 +3,7 @@ use crate::chunk_system::generation_cache::Cache;
use crate::generation::height_limit::HeightLimitView; use crate::generation::height_limit::HeightLimitView;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
#[inline]
const fn get_chunk_index(cache: &Cache, chunk_x: i32, chunk_z: i32) -> Option<usize> { const fn get_chunk_index(cache: &Cache, chunk_x: i32, chunk_z: i32) -> Option<usize> {
let rel_x = chunk_x - cache.x; let rel_x = chunk_x - cache.x;
let rel_z = chunk_z - cache.z; 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) Some((rel_x * cache.size + rel_z) as usize)
} }
#[inline]
fn get_section_y(cache: &Cache, pos_y: i32) -> Option<usize> { fn get_section_y(cache: &Cache, pos_y: i32) -> Option<usize> {
let bottom = cache.bottom_y() as i32; let bottom = cache.bottom_y() as i32;
if pos_y < bottom { if pos_y < bottom {
@ -21,6 +23,7 @@ fn get_section_y(cache: &Cache, pos_y: i32) -> Option<usize> {
Some(section) Some(section)
} }
#[inline]
#[must_use] #[must_use]
pub fn get_block_light(cache: &Cache, pos: BlockPos) -> u8 { pub fn get_block_light(cache: &Cache, pos: BlockPos) -> u8 {
let chunk_x = pos.0.x >> 4; 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) { pub fn set_block_light(cache: &mut Cache, pos: BlockPos, level: u8) {
let chunk_x = pos.0.x >> 4; let chunk_x = pos.0.x >> 4;
let chunk_z = pos.0.z >> 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] #[must_use]
pub fn get_sky_light(cache: &Cache, pos: BlockPos) -> u8 { pub fn get_sky_light(cache: &Cache, pos: BlockPos) -> u8 {
let chunk_x = pos.0.x >> 4; 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) { pub fn set_sky_light(cache: &mut Cache, pos: BlockPos, level: u8) {
let chunk_x = pos.0.x >> 4; let chunk_x = pos.0.x >> 4;
let chunk_z = pos.0.z >> 4; let chunk_z = pos.0.z >> 4;

View File

@ -1,4 +1,4 @@
use std::collections::HashMap; use rustc_hash::FxHashMap;
use std::io::{Cursor, Read, Write}; use std::io::{Cursor, Read, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tracing::{info, warn}; use tracing::{info, warn};
@ -70,16 +70,16 @@ pub struct PoiSectionData {
pub struct PoiChunkData { pub struct PoiChunkData {
pub data_version: i32, pub data_version: i32,
/// Sections keyed by Y section coordinate (e.g., "-1", "0", "1", "4") /// 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 /// POI data for a single region (32x32 chunks) using MCA format
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct PoiRegion { pub struct PoiRegion {
/// Entries indexed by position /// Entries indexed by position
entries: HashMap<(i32, i32, i32), PoiEntry>, entries: FxHashMap<(i32, i32, i32), PoiEntry>,
/// Track which chunks are dirty /// Track which chunks are dirty
dirty_chunks: std::collections::HashSet<(i32, i32)>, dirty_chunks: rustc_hash::FxHashSet<(i32, i32)>,
dirty: bool, dirty: bool,
} }
@ -144,7 +144,7 @@ impl PoiRegion {
/// Group entries by chunk, then create chunk NBT data /// Group entries by chunk, then create chunk NBT data
fn get_chunk_data(&self, chunk_x: i32, chunk_z: i32) -> Option<PoiChunkData> { 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() { for entry in self.entries.values() {
let entry_chunk_x = entry.x >> 4; 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()))?; .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 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") { if let Some(sec_tag) = nbt.get_compound("Sections") {
for (sec_key, tag) in &sec_tag.child_tags { for (sec_key, tag) in &sec_tag.child_tags {
@ -269,11 +269,11 @@ impl PoiRegion {
} }
// Build all chunk data // 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 // Collect all unique chunks that have entries
let mut chunks_with_data: std::collections::HashSet<(i32, i32)> = let mut chunks_with_data: rustc_hash::FxHashSet<(i32, i32)> =
std::collections::HashSet::new(); rustc_hash::FxHashSet::default();
for entry in self.entries.values() { for entry in self.entries.values() {
chunks_with_data.insert((entry.x >> 4, entry.z >> 4)); chunks_with_data.insert((entry.x >> 4, entry.z >> 4));
} }
@ -429,7 +429,7 @@ pub struct PoiStorage {
/// Path to the poi folder /// Path to the poi folder
folder: PathBuf, folder: PathBuf,
/// Loaded regions, keyed by (`region_x`, `region_z`) /// Loaded regions, keyed by (`region_x`, `region_z`)
regions: HashMap<(i32, i32), PoiRegion>, regions: FxHashMap<(i32, i32), PoiRegion>,
} }
impl PoiStorage { impl PoiStorage {
@ -437,7 +437,7 @@ impl PoiStorage {
pub fn new(poi_folder: PathBuf) -> Self { pub fn new(poi_folder: PathBuf) -> Self {
Self { Self {
folder: poi_folder, folder: poi_folder,
regions: HashMap::new(), regions: FxHashMap::default(),
} }
} }

View File

@ -76,7 +76,7 @@ wasmparser.workspace = true
wasm-encoder = "0.257" wasm-encoder = "0.257"
# authentication # authentication
ureq = { workspace = true, features = ["json"] } reqwest = { workspace = true, features = ["json"] }
sha1.workspace = true sha1.workspace = true

View File

@ -1,4 +1,4 @@
use std::collections::HashSet; use rustc_hash::FxHashSet;
use crate::block::{ use crate::block::{
BlockBehaviour, BlockIsReplacing, BlockMetadata, CanPlaceAtArgs, CanUpdateAtArgs, BlockBehaviour, BlockIsReplacing, BlockMetadata, CanPlaceAtArgs, CanUpdateAtArgs,
@ -154,7 +154,7 @@ fn get_attach_direction(
replacing_block, replacing_block,
)) ))
} else { } else {
HashSet::new() FxHashSet::default()
}; };
if let Some(player) = player_wrapper { if let Some(player) = player_wrapper {
@ -183,8 +183,8 @@ const fn is_solid_face(block: &Block) -> bool {
block.default_state.is_full_cube() block.default_state.is_full_cube()
} }
fn active_directions(props: GlowLichenLikeProperties) -> HashSet<BlockDirection> { fn active_directions(props: GlowLichenLikeProperties) -> FxHashSet<BlockDirection> {
let mut set = HashSet::new(); let mut set = FxHashSet::default();
if props.down { if props.down {
set.insert(BlockDirection::Down); set.insert(BlockDirection::Down);
} }

View File

@ -1,17 +1,9 @@
use std::sync::Arc; 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_macros::pumpkin_block;
use pumpkin_world::world::BlockFlags;
use crate::block::entities::trial_spawner::TrialSpawnerBlockEntity; use crate::block::entities::trial_spawner::TrialSpawnerBlockEntity;
use crate::block::registry::BlockActionResult; use crate::block::{BlockBehaviour, PlacedArgs};
use crate::block::{BlockBehaviour, NormalUseArgs, PlacedArgs, UseWithItemArgs};
#[pumpkin_block("minecraft:trial_spawner")] #[pumpkin_block("minecraft:trial_spawner")]
pub struct TrialSpawnerBlock; pub struct TrialSpawnerBlock;
@ -21,95 +13,4 @@ impl BlockBehaviour for TrialSpawnerBlock {
let entity = TrialSpawnerBlockEntity::new(*args.position); let entity = TrialSpawnerBlockEntity::new(*args.position);
args.world.add_block_entity(Arc::new(entity)); 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,
})
}
} }

View File

@ -6,11 +6,7 @@ use std::sync::{
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
use pumpkin_data::{entity::EntityType, world::WorldEvent}; use pumpkin_data::{entity::EntityType, world::WorldEvent};
use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::compound::NbtCompound;
use pumpkin_util::math::{ use pumpkin_util::math::{position::BlockPos, vector3::Vector3};
boundingbox::{BoundingBox, EntityDimensions},
position::BlockPos,
vector3::Vector3,
};
use crate::{block::entities::BlockEntity, entity::EntityBase, world::World}; 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 + (rand::random::<f64>() - rand::random::<f64>()) * spawn_range as f64
+ 0.5, + 0.5,
); );
// TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes if !world.is_space_empty(entity_type.get_spawn_bounding_box(
if !world.is_space_empty(BoundingBox::new_from_pos(
spawn_pos.x, spawn_pos.x,
spawn_pos.y, spawn_pos.y,
spawn_pos.z, spawn_pos.z,
&EntityDimensions {
width: entity_type.dimension[0],
height: entity_type.dimension[1],
eye_height: entity_type.eye_height,
},
)) { )) {
continue; continue;
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
use super::BlockEntity; use super::BlockEntity;
use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::compound::NbtCompound;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use std::collections::HashSet; use rustc_hash::FxHashSet;
use std::sync::Mutex; use std::sync::Mutex;
use uuid::Uuid; use uuid::Uuid;
@ -9,7 +9,7 @@ pub struct VaultBlockEntity {
pub position: BlockPos, pub position: BlockPos,
pub config: Mutex<Option<NbtCompound>>, pub config: Mutex<Option<NbtCompound>>,
pub server_data: 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 { impl BlockEntity for VaultBlockEntity {
@ -29,7 +29,7 @@ impl BlockEntity for VaultBlockEntity {
position, position,
config: Mutex::new(nbt.get_compound("config").cloned()), config: Mutex::new(nbt.get_compound("config").cloned()),
server_data: Mutex::new(nbt.get_compound("server_data").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, position,
config: Mutex::new(None), config: Mutex::new(None),
server_data: Mutex::new(None), server_data: Mutex::new(None),
rewarded_players: Mutex::new(HashSet::new()), rewarded_players: Mutex::new(FxHashSet::default()),
} }
} }

View File

@ -14,7 +14,7 @@ use crate::{
dispatcher::CommandError, dispatcher::CommandError,
tree::{RawArg, RawArgs}, tree::{RawArg, RawArgs},
}, },
net::authentication::lookup_profile_by_name, net::authentication::lookup_profile_by_name_blocking,
net::{GameProfile, offline_uuid}, net::{GameProfile, offline_uuid},
server::Server, server::Server,
}; };
@ -254,7 +254,7 @@ fn resolve_profiles_from_token(
} }
if server.advanced_config.networking.java.online_mode { if server.advanced_config.networking.java.online_mode {
match lookup_profile_by_name( match lookup_profile_by_name_blocking(
raw_arg.value, raw_arg.value,
&server.advanced_config.networking.java.authentication, &server.advanced_config.networking.java.authentication,
) { ) {

View File

@ -10,7 +10,7 @@ use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::errors::error_types::CommandErrorType; use crate::command::errors::error_types::CommandErrorType;
use crate::command::string_reader::StringReader; use crate::command::string_reader::StringReader;
use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; 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::net::{GameProfile, offline_uuid};
use crate::server::Server; use crate::server::Server;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
@ -72,7 +72,7 @@ impl GameProfileResult {
} }
if server.advanced_config.networking.java.online_mode { if server.advanced_config.networking.java.online_mode {
return match lookup_profile_by_name( return match lookup_profile_by_name_blocking(
name, name,
&server.advanced_config.networking.java.authentication, &server.advanced_config.networking.java.authentication,
) { ) {

View File

@ -142,7 +142,7 @@ fn report_resolved_profile(
source.send_feedback(msg, false); 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) { if let Some(player) = server.get_player_by_name(name) {
return Some(player.gameprofile.clone()); return Some(player.gameprofile.clone());
} }
@ -155,7 +155,10 @@ fn fetch_profile_by_name_helper(server: &Server, name: &str) -> Option<GameProfi
.java .java
.authentication .authentication
.clone(); .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 { if let Some((uuid, resolved_name)) = mojang_res {
server server
@ -171,6 +174,7 @@ fn fetch_profile_by_name_helper(server: &Server, name: &str) -> Option<GameProfi
.authentication .authentication
.clone(); .clone();
let full_profile = fetch_profile_by_uuid(uuid, &auth_config_clone) let full_profile = fetch_profile_by_uuid(uuid, &auth_config_clone)
.await
.ok() .ok()
.flatten(); .flatten();
@ -212,7 +216,7 @@ fn fetch_profile_by_name_helper(server: &Server, name: &str) -> Option<GameProfi
None 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) { if let Some(player) = server.get_player_by_uuid(id) {
return Some(player.gameprofile.clone()); return Some(player.gameprofile.clone());
} }
@ -223,7 +227,7 @@ fn fetch_profile_by_id_helper(server: &Server, id: Uuid) -> Option<GameProfile>
.java .java
.authentication .authentication
.clone(); .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 { if let Some(profile) = mojang_res {
server server
@ -258,8 +262,8 @@ impl CommandExecutor for ResolveNameExecutor {
let name_owned = name.to_string(); let name_owned = name.to_string();
let name_component = TextComponent::text(name_owned.clone()); let name_component = TextComponent::text(name_owned.clone());
rayon::spawn(move || { tokio::spawn(async move {
let result = fetch_profile_by_name_helper(&server, &name_owned); let result = fetch_profile_by_name_helper(&server, &name_owned).await;
match result { match result {
Some(profile) => { Some(profile) => {
report_resolved_profile( report_resolved_profile(
@ -292,8 +296,8 @@ impl CommandExecutor for ResolveIdExecutor {
let source = context.source.clone(); let source = context.source.clone();
let id_component = TextComponent::text(id.to_string()); let id_component = TextComponent::text(id.to_string());
rayon::spawn(move || { tokio::spawn(async move {
let result = fetch_profile_by_id_helper(&server, id); let result = fetch_profile_by_id_helper(&server, id).await;
match result { match result {
Some(profile) => { Some(profile) => {
report_resolved_profile( report_resolved_profile(

View File

@ -13,7 +13,7 @@ use pumpkin_util::PermissionLvl;
use pumpkin_util::math::vector2::Vector2; use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use std::collections::HashMap; use rustc_hash::FxHashMap;
const DESCRIPTION: &str = "Changes biomes of an area."; const DESCRIPTION: &str = "Changes biomes of an area.";
const PERMISSION: &str = "minecraft:command.fillbiome"; const PERMISSION: &str = "minecraft:command.fillbiome";
@ -72,8 +72,8 @@ impl CommandExecutor for FillBiomeExecutor {
None None
}; };
let mut chunk_modifications: HashMap<Vector2<i32>, Vec<(usize, usize, usize)>> = let mut chunk_modifications: FxHashMap<Vector2<i32>, Vec<(usize, usize, usize)>> =
HashMap::new(); FxHashMap::default();
for y in biome_min_y..=biome_max_y { for y in biome_min_y..=biome_max_y {
for z in biome_min_z..=biome_max_z { for z in biome_min_z..=biome_max_z {
for x in biome_min_x..=biome_max_x { for x in biome_min_x..=biome_max_x {

View File

@ -51,7 +51,14 @@ fn fetch_all_contributors_cached() -> Vec<Contributor> {
return cache.data.clone(); 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 !contributors.is_empty() {
if let Ok(mut guard) = CONTRIBUTORS_CACHE.lock() { if let Ok(mut guard) = CONTRIBUTORS_CACHE.lock() {
*guard = Some(ContributorCache { *guard = Some(ContributorCache {
@ -68,25 +75,35 @@ fn fetch_all_contributors_cached() -> Vec<Contributor> {
contributors 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 all_contributors = Vec::new();
let mut next_url = Some( let mut next_url = Some(
"https://api.github.com/repos/Pumpkin-MC/Pumpkin/contributors?per_page=100".to_string(), "https://api.github.com/repos/Pumpkin-MC/Pumpkin/contributors?per_page=100".to_string(),
); );
while let Some(url) = next_url { 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 { match response {
Ok(mut res) => { Ok(res) => {
if let Ok(contributors) = res.body_mut().read_json::<Vec<Contributor>>() { 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); all_contributors.extend(contributors);
} else { } else {
break; 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, 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 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"); let mut donators_text = TextComponent::text("Click to open Donate\n\nDonators:\n");
if let Ok(mut res) = response if let Ok(res) = response
&& let Ok(data) = res.body_mut().read_json::<DonatorResponse>() && let Ok(data) = res.json::<DonatorResponse>().await
{ {
let mut current = data.current; let mut current = data.current;
current.sort_by(|a, b| { current.sort_by(|a, b| {
@ -225,7 +246,16 @@ fn fetch_donators_hover_cached() -> TextComponent {
return cache.data.clone(); 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() { if let Ok(mut guard) = DONATORS_CACHE.lock() {
*guard = Some(DonatorCache { *guard = Some(DonatorCache {
fetched_at: Instant::now(), fetched_at: Instant::now(),

View File

@ -1,5 +1,5 @@
use rustc_hash::FxHashMap;
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::HashMap;
use std::{env, fs}; use std::{env, fs};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -29,8 +29,8 @@ struct UserCacheEntryDisk {
#[derive(Default)] #[derive(Default)]
pub struct UserCache { pub struct UserCache {
profiles_by_name: HashMap<String, UserCacheEntry>, profiles_by_name: FxHashMap<String, UserCacheEntry>,
profiles_by_uuid: HashMap<Uuid, UserCacheEntry>, profiles_by_uuid: FxHashMap<Uuid, UserCacheEntry>,
operation_count: u64, operation_count: u64,
} }

View File

@ -1,6 +1,6 @@
use pumpkin_data::attributes::Attributes; use pumpkin_data::attributes::Attributes;
use pumpkin_data::entity::EntityType; use pumpkin_data::entity::EntityType;
use std::collections::HashMap; use rustc_hash::FxHashMap;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64; use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
@ -193,10 +193,10 @@ impl Clone for AttributeInstance {
} }
/// Registry storing per-entity-type base attribute overrides. /// 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)] #[derive(Default)]
pub struct AttributeRegistry { pub struct AttributeRegistry {
map: HashMap<u16, HashMap<u8, f64>>, map: FxHashMap<u16, FxHashMap<u8, f64>>,
} }
impl AttributeRegistry { impl AttributeRegistry {
@ -278,7 +278,7 @@ mod tests {
.find(|(attr, _)| attr.id == Attributes::MOVEMENT_SPEED.id); .find(|(attr, _)| attr.id == Attributes::MOVEMENT_SPEED.id);
assert!(speed_attr.is_some()); assert!(speed_attr.is_some());
let (_, base_speed) = speed_attr.unwrap(); let (_, base_speed) = speed_attr.unwrap();
assert!((base_speed - 0.1).abs() < f64::EPSILON); assert!((base_speed - 0.1).abs() < 1e-4);
} }
#[test] #[test]

View File

@ -1,11 +1,10 @@
use std::collections::HashSet;
use pumpkin_data::Block; use pumpkin_data::Block;
use pumpkin_data::damage::DamageType; use pumpkin_data::damage::DamageType;
use pumpkin_data::world::WorldEvent; use pumpkin_data::world::WorldEvent;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::world::BlockFlags; use pumpkin_world::world::BlockFlags;
use rustc_hash::FxHashSet;
use crate::entity::effect::MobEffect; use crate::entity::effect::MobEffect;
use crate::entity::living::LivingEntity; use crate::entity::living::LivingEntity;
@ -25,7 +24,7 @@ impl MobEffect for WeavingMobEffect {
let center_pos = living.entity.block_pos.load(); let center_pos = living.entity.block_pos.load();
let cobweb_count = (rand::random::<u32>() % 2 + 2) as usize; // 2 to 3 cobwebs 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 // Sample up to 15 random positions in a cube of radius 1
for _ in 0..15 { for _ in 0..15 {

View File

@ -1,4 +1,4 @@
use std::collections::HashSet; use rustc_hash::FxHashSet;
use std::sync::{ use std::sync::{
Arc, Mutex, Arc, Mutex,
atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering}, atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering},
@ -27,7 +27,7 @@ pub struct LightningBoltEntity {
flashes: AtomicI32, flashes: AtomicI32,
visual_only: AtomicBool, visual_only: AtomicBool,
cause: Mutex<Option<Arc<Player>>>, cause: Mutex<Option<Arc<Player>>>,
hit_entities: Mutex<HashSet<i32>>, hit_entities: Mutex<FxHashSet<i32>>,
blocks_set_on_fire: AtomicI32, blocks_set_on_fire: AtomicI32,
} }
@ -43,7 +43,7 @@ impl LightningBoltEntity {
flashes: AtomicI32::new(flashes), flashes: AtomicI32::new(flashes),
visual_only: AtomicBool::new(false), visual_only: AtomicBool::new(false),
cause: Mutex::new(None), cause: Mutex::new(None),
hit_entities: Mutex::new(HashSet::new()), hit_entities: Mutex::new(FxHashSet::default()),
blocks_set_on_fire: AtomicI32::new(0), blocks_set_on_fire: AtomicI32::new(0),
} }
} }

View File

@ -12,13 +12,14 @@ use pumpkin_protocol::codec::var_ulong::VarULong;
use pumpkin_util::GameMode; use pumpkin_util::GameMode;
use pumpkin_util::Hand; use pumpkin_util::Hand;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use rustc_hash::FxHashMap;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::atomic::{ use std::sync::atomic::{
AtomicBool, AtomicU8, AtomicBool, AtomicU8,
Ordering::{Relaxed, SeqCst}, Ordering::{Relaxed, SeqCst},
}; };
use std::{collections::HashMap, sync::atomic::AtomicI32};
use tracing::warn; use tracing::warn;
use super::experience_orb::ExperienceOrbEntity; use super::experience_orb::ExperienceOrbEntity;
@ -59,7 +60,7 @@ use pumpkin_protocol::java::client::play::{
}; };
use pumpkin_protocol::{ use pumpkin_protocol::{
codec::item_stack_seralizer::ItemStackSerializer, codec::item_stack_seralizer::ItemStackSerializer,
java::client::play::{CDamageEvent, CSetEquipment, Metadata, MetadataSerializer}, java::client::play::{CSetEquipment, Metadata, MetadataSerializer},
ser::{NetworkWriteExt, WritingError}, ser::{NetworkWriteExt, WritingError},
}; };
use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::boundingbox::BoundingBox;
@ -90,11 +91,11 @@ pub struct LivingEntity {
pub dead: AtomicBool, pub dead: AtomicBool,
/// The distance the entity has been falling. /// The distance the entity has been falling.
pub fall_distance: AtomicCell<f32>, 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 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 movement_input: AtomicCell<Vector3<f64>>,
pub equipment_slots: Arc<HashMap<usize, EquipmentSlot>>, pub equipment_slots: Arc<FxHashMap<usize, EquipmentSlot>>,
pub jumping: AtomicBool, pub jumping: AtomicBool,
@ -119,7 +120,7 @@ pub struct LivingEntity {
livings_flags: AtomicU8, livings_flags: AtomicU8,
/// The attributes of the entity /// The attributes of the entity
pub attributes: RwLock<HashMap<u8, AttributeInstance>>, pub attributes: RwLock<FxHashMap<u8, AttributeInstance>>,
} }
struct EffectParticle { struct EffectParticle {
@ -186,7 +187,7 @@ impl LivingEntity {
Self { Self {
// Populate local attribute instances from the default registry and get initial vars // Populate local attribute instances from the default registry and get initial vars
attributes: { attributes: {
let mut m = std::collections::HashMap::new(); let mut m = FxHashMap::default();
for (attr, base) in entity.entity_type.attributes { for (attr, base) in entity.entity_type.attributes {
if attr.id == Attributes::MAX_HEALTH.id { if attr.id == Attributes::MAX_HEALTH.id {
@ -208,9 +209,9 @@ impl LivingEntity {
item_in_use: std::sync::Mutex::new(None), item_in_use: std::sync::Mutex::new(None),
active_hand: std::sync::Mutex::new(None), active_hand: std::sync::Mutex::new(None),
livings_flags: AtomicU8::new(0), 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())), 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()), equipment_slots: Arc::new(build_equipment_slots()),
jumping: AtomicBool::new(false), jumping: AtomicBool::new(false),
jumping_cooldown: AtomicU8::new(0), jumping_cooldown: AtomicU8::new(0),
@ -258,8 +259,8 @@ impl LivingEntity {
selected_slot: 0, selected_slot: 0,
container_id: window_id, container_id: window_id,
}; };
self.entity.world.load().broadcast_packet_except_editioned( self.entity.world.load().send_to_tracking_players_editioned(
&[self.entity.entity_uuid], &self.entity,
&je_packet, &je_packet,
&be_packet, &be_packet,
); );
@ -271,7 +272,7 @@ impl LivingEntity {
self.entity self.entity
.world .world
.load() .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), data: VarInt(0),
fire_at_position: None, fire_at_position: None,
}; };
world.broadcast_to_chunk( let hurt_animation = CHurtAnimation::new(entity_id.into(), hurt_yaw);
self.entity.chunk_pos.load(), world.send_to_tracking_players_and_self_editioned(
&CHurtAnimation::new(entity_id.into(), hurt_yaw), &self.entity,
&hurt_animation,
&hurt_event,
); );
world.broadcast_to_chunk_bedrock(self.entity.chunk_pos.load(), &hurt_event);
} }
world.broadcast_packet_all(&CDamageEvent::new( world.broadcast_damage_event(
self.entity.entity_id.into(), &self.entity,
damage_type.id.into(), i32::from(damage_type.id),
source.map(|e| e.get_entity().entity_id.into()), source.map(|e| e.get_entity().entity_id),
cause.map(|e| e.get_entity().entity_id.into()), cause.map(|e| e.get_entity().entity_id),
position, position,
)); );
if play_sound { if play_sound {
world.play_sound( world.play_sound(

View File

@ -2989,20 +2989,33 @@ impl Entity {
bedrock_meta: Option<&SyncedActorDataList>, bedrock_meta: Option<&SyncedActorDataList>,
) { ) {
let world = self.world.load(); let world = self.world.load();
let chunk_pos = self.chunk_pos.load();
let players = world.players.load(); let players = world.players.load();
let mut java_recipients = Vec::new(); let mut java_recipients = Vec::new();
let mut bedrock_recipients = Vec::new(); let mut bedrock_recipients = Vec::new();
for player in players.iter() { if let Some(tracked) = world.entity_tracker.get_tracked_entity(self.entity_id) {
let center = player.get_entity().chunk_pos.load(); for player in players.iter() {
let view_distance = crate::world::chunker::get_view_distance(player).get() as i32; 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;
if is_within_view_distance(chunk_pos, center, view_distance) { if is_within_view_distance(chunk_pos, center, view_distance) {
match player.client.as_ref() { match player.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(player), ClientPlatform::Java(_) => java_recipients.push(player),
ClientPlatform::Bedrock(client) => bedrock_recipients.push(client), ClientPlatform::Bedrock(client) => bedrock_recipients.push(client),
}
} }
} }
} }

View File

@ -1,4 +1,4 @@
use std::collections::HashMap; use rustc_hash::FxHashMap;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering}; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use uuid::Uuid; use uuid::Uuid;
@ -264,7 +264,7 @@ pub struct VillagerEntity {
pub restocks_today: AtomicI32, pub restocks_today: AtomicI32,
pub last_gossip_decay_time: AtomicI64, pub last_gossip_decay_time: AtomicI64,
pub last_gossip_share_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 inventory: std::sync::Mutex<Vec<ItemStack>>,
pub merchant_inventory: Arc<SimpleInventory>, pub merchant_inventory: Arc<SimpleInventory>,
pub offers: std::sync::Mutex<Vec<pumpkin_protocol::java::client::play::MerchantOffer>>, pub offers: std::sync::Mutex<Vec<pumpkin_protocol::java::client::play::MerchantOffer>>,
@ -333,7 +333,7 @@ impl VillagerEntity {
restocks_today: AtomicI32::new(0), restocks_today: AtomicI32::new(0),
last_gossip_decay_time: AtomicI64::new(0), last_gossip_decay_time: AtomicI64::new(0),
last_gossip_share_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, inventory,
merchant_inventory: Arc::new(SimpleInventory::new(3)), merchant_inventory: Arc::new(SimpleInventory::new(3)),
offers: std::sync::Mutex::new(Vec::new()), offers: std::sync::Mutex::new(Vec::new()),

View File

@ -2,7 +2,7 @@ pub mod advancement;
pub mod statistics; pub mod statistics;
use core::f32; use core::f32;
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::f64::consts::TAU; use std::f64::consts::TAU;
use std::num::NonZero; use std::num::NonZero;
use std::str::FromStr; use std::str::FromStr;
@ -41,7 +41,7 @@ use pumpkin_protocol::bedrock::server::{
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_util::translation::Locale; use pumpkin_util::translation::Locale;
use pumpkin_util::version::JavaMinecraftVersion; use pumpkin_util::version::JavaMinecraftVersion;
use pumpkin_world::chunk::{ChunkData, ChunkEntityData}; use pumpkin_world::chunk::ChunkData;
use pumpkin_world::inventory::Inventory; use pumpkin_world::inventory::Inventory;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{debug, warn}; use tracing::{debug, warn};
@ -284,7 +284,6 @@ use pumpkin_util::text::hover::HoverEvent;
use pumpkin_util::{GameMode, Hand}; use pumpkin_util::{GameMode, Hand};
use pumpkin_world::biome; use pumpkin_world::biome;
use pumpkin_world::cylindrical_chunk_iterator::Cylindrical; use pumpkin_world::cylindrical_chunk_iterator::Cylindrical;
use pumpkin_world::level::{Level, SyncChunk, SyncEntityChunk};
use crate::block; use crate::block;
use crate::block::blocks::bed::BedBlock; use crate::block::blocks::bed::BedBlock;
@ -312,7 +311,6 @@ use super::item::ItemEntity;
use super::living::LivingEntity; use super::living::LivingEntity;
use super::{Entity, EntityBase, NBTStorage, NBTStorageInit}; use super::{Entity, EntityBase, NBTStorage, NBTStorageInit};
use pumpkin_data::potion::Effect; use pumpkin_data::potion::Effect;
use pumpkin_world::chunk_system::ChunkLoading;
const MAX_CACHED_SIGNATURES: u8 = 128; // Vanilla: 128 const MAX_CACHED_SIGNATURES: u8 = 128; // Vanilla: 128
const MAX_PREVIOUS_MESSAGES: u8 = 20; // Vanilla: 20 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. /// Represents a Minecraft player entity.
/// ///
/// A `Player` is a special type of entity that represents a human player connected to the server. /// 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 experience_points: AtomicI32,
pub item_cooldowns: std::sync::Mutex<HashMap<String, ItemCooldown>>, pub item_cooldowns: std::sync::Mutex<HashMap<String, ItemCooldown>>,
pub experience_pick_up_delay: Mutex<u32>, 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, pub has_played_before: AtomicBool,
root_vehicle_uuid: AtomicCell<Option<Uuid>>, root_vehicle_uuid: AtomicCell<Option<Uuid>>,
pub chat_session: Arc<Mutex<ChatSession>>, pub chat_session: Arc<Mutex<ChatSession>>,
@ -836,7 +519,6 @@ pub struct Player {
use base64::prelude::*; use base64::prelude::*;
use pumpkin_protocol::Property; use pumpkin_protocol::Property;
use serde::Deserialize; use serde::Deserialize;
use std::io::Read;
// Bit masks for the Java skin pixels that Bedrock requires to be opaque. // Bit masks for the Java skin pixels that Bedrock requires to be opaque.
// Adapted from Geyser's SkinProvider under the MIT License. // Adapted from Geyser's SkinProvider under the MIT License.
@ -883,10 +565,20 @@ impl Player {
.and_then(|m| m.model.as_deref()) .and_then(|m| m.model.as_deref())
.is_some_and(|model| model == "slim"); .is_some_and(|model| model == "slim");
let resp = ureq::get(&url).call().ok()?; let bytes = if let Ok(handle) = tokio::runtime::Handle::try_current() {
let mut buf = Vec::new(); tokio::task::block_in_place(|| {
resp.into_body().into_reader().read_to_end(&mut buf).ok()?; handle.block_on(async {
let img = image::load_from_memory(&buf).ok()?; 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 width = img.width();
let height = img.height(); let height = img.height();
@ -1081,12 +773,10 @@ impl Player {
experience_progress: AtomicCell::new(0.0), experience_progress: AtomicCell::new(0.0),
experience_points: AtomicI32::new(0), experience_points: AtomicI32::new(0),
item_cooldowns: std::sync::Mutex::new(HashMap::new()), item_cooldowns: std::sync::Mutex::new(HashMap::new()),
// Default to sending 16 chunks per tick. chunk_sender: Mutex::new(crate::net::ChunkSender::new()),
chunk_manager: Mutex::new(ChunkManager::new( chunk_listener: Mutex::new(world.level.chunk_listener.add_global_chunk_listener()),
16, held_chunk_tickets: Mutex::new(None),
world.level.chunk_listener.add_global_chunk_listener(), chunk_send_epoch: AtomicU32::new(0),
world.clone(),
)),
last_sent_xp: AtomicI32::new(-1), last_sent_xp: AtomicI32::new(-1),
last_sent_health: AtomicI32::new(-1), last_sent_health: AtomicI32::new(-1),
last_sent_food: AtomicU8::new(0), last_sent_food: AtomicU8::new(0),
@ -1341,10 +1031,10 @@ impl Player {
world.remove_player(self, true).await; world.remove_player(self, true).await;
let cylindrical = self.watched_section.load(); let cylindrical = self.watched_section.load();
self.chunk_manager self.clean_up_chunk_tickets(&world.level);
.lock() if let Ok(mut sender) = self.chunk_sender.lock() {
.unwrap_or_else(std::sync::PoisonError::into_inner) sender.reset();
.clean_up(&world.level); }
// Radial chunks are all of the chunks the player is theoretically viewing. // Radial chunks are all of the chunks the player is theoretically viewing.
// Given enough time, all of these chunks will be in memory. // Given enough time, all of these chunks will be in memory.
@ -1396,6 +1086,41 @@ impl Player {
.add_passenger(vehicle.clone(), self.clone()); .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)] #[expect(clippy::too_many_lines)]
pub fn attack(&self, victim: &Arc<dyn EntityBase>) { pub fn attack(&self, victim: &Arc<dyn EntityBase>) {
let world = self.world(); let world = self.world();
@ -2602,48 +2327,72 @@ impl Player {
{ {
*xp -= 1; *xp -= 1;
} }
let (chunk_of_chunks, total_sent_chunks) = self.chunk_manager.try_lock().map_or_else( if let Ok(listener) = self.chunk_listener.try_lock()
|_| (None, 0), && let Ok(mut sender) = self.chunk_sender.try_lock()
|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()
{ {
let client = self.client.clone(); let center = self.get_entity().chunk_pos.load();
self.spawn_task(async move { let view_dist =
client.send_chunks(&chunk_of_chunks).await; std::num::NonZeroI32::from(self.watched_section.load().view_distance).get();
}); while let Ok((pos, _)) = listener.try_recv() {
if let ClientPlatform::Bedrock(bedrock_client) = self.client.as_ref() if (pos.x - center.x).abs().max((pos.y - center.y).abs()) <= view_dist {
&& !self.bedrock_spawned.load(Ordering::Relaxed) sender.enqueue_chunk(pos);
&& total_sent_chunks > 4
{
if let Ok(data) = bedrock_client.serialize_packet(&CPlayStatus::PlayerSpawn) {
bedrock_client.try_enqueue_packet(data);
} }
self.bedrock_spawned.store(true, Ordering::Relaxed); }
self.set_client_loaded(true); }
self.send_health();
if self.living_entity.health.load() <= 0.0 { let world = self.world();
self.send_bedrock_respawn_state(RespawnState::SearchingForSpawn); 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(&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
{
if let Ok(data) = bedrock_client.serialize_packet(&CPlayStatus::PlayerSpawn) {
bedrock_client.try_enqueue_packet(data);
}
self.bedrock_spawned.store(true, Ordering::Relaxed);
self.set_client_loaded(true);
self.send_health();
if self.living_entity.health.load() <= 0.0 {
self.send_bedrock_respawn_state(RespawnState::SearchingForSpawn);
} }
} }
self.tick_counter.fetch_add(1, Ordering::Relaxed); self.tick_counter.fetch_add(1, Ordering::Relaxed);
@ -3656,10 +3405,7 @@ impl Player {
}); });
self.unload_watched_chunks(&current_world).await; self.unload_watched_chunks(&current_world).await;
self.chunk_manager self.change_world_chunks(&current_world.level, &new_world);
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.change_world(&current_world.level, new_world.clone());
self.living_entity.entity.set_world(new_world.clone()); self.living_entity.entity.set_world(new_world.clone());
if new_world.dimension == pumpkin_data::dimension::Dimension::THE_NETHER { 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); 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; let teleport_id = i + 1;
self.living_entity.entity.set_pos(position); self.living_entity.entity.set_pos(position);
let entity = &self.living_entity.entity; let entity = &self.living_entity.entity;

View File

@ -1,12 +1,12 @@
pub use pumpkin_data::statistic::{CustomStatistic, StatisticCategory}; pub use pumpkin_data::statistic::{CustomStatistic, StatisticCategory};
use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::tag::NbtTag; use pumpkin_nbt::tag::NbtTag;
use std::collections::HashMap; use rustc_hash::FxHashMap;
#[derive(Default)] #[derive(Default)]
pub struct Statistics { pub struct Statistics {
/// (Category ID, Statistic ID) -> Value /// (Category ID, Statistic ID) -> Value
pub stats: HashMap<(i32, i32), i32>, pub stats: FxHashMap<(i32, i32), i32>,
} }
impl Statistics { impl Statistics {

View File

@ -3,11 +3,11 @@ use std::{collections::HashMap, net::IpAddr};
use base64::{Engine, engine::general_purpose}; use base64::{Engine, engine::general_purpose};
use pumpkin_config::{AuthenticationConfig, networking::auth::TextureConfig}; use pumpkin_config::{AuthenticationConfig, networking::auth::TextureConfig};
use pumpkin_protocol::Property; use pumpkin_protocol::Property;
use reqwest::{StatusCode, Url};
use rsa::RsaPublicKey; use rsa::RsaPublicKey;
use rsa::pkcs8::DecodePublicKey; use rsa::pkcs8::DecodePublicKey;
use serde::Deserialize; use serde::Deserialize;
use thiserror::Error; use thiserror::Error;
use ureq::http::{StatusCode, Uri};
use uuid::Uuid; use uuid::Uuid;
use super::GameProfile; use super::GameProfile;
@ -54,16 +54,16 @@ const MOJANG_PROFILE_BY_NAME_URL: &str =
const MOJANG_PROFILE_BY_UUID_URL: &str = const MOJANG_PROFILE_BY_UUID_URL: &str =
"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false"; "https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false";
fn create_agent(auth_config: &AuthenticationConfig) -> ureq::Agent { fn create_client(auth_config: &AuthenticationConfig) -> reqwest::Client {
let config = ureq::Agent::config_builder() reqwest::Client::builder()
.timeout_connect(Some(std::time::Duration::from_millis( .connect_timeout(std::time::Duration::from_millis(
auth_config.connect_timeout as u64, 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, auth_config.read_timeout as u64,
))) ))
.build(); .build()
config.into() .unwrap_or_default()
} }
fn format_auth_url(url_template: &str, username: &str, server_hash: &str, ip: &IpAddr) -> String { 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. /// 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> /// See <https://pumpkinmc.org/developer/networking/authentication>
pub fn authenticate( pub async fn authenticate(
username: &str, username: &str,
server_hash: &str, server_hash: &str,
ip: &IpAddr, ip: &IpAddr,
@ -111,7 +111,7 @@ pub fn authenticate(
candidate_urls.push(fallback.as_str()); 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 unverified_count = 0;
let mut last_unknown_status = None; let mut last_unknown_status = None;
@ -119,7 +119,7 @@ pub fn authenticate(
for url_template in candidate_urls { for url_template in candidate_urls {
let address = format_auth_url(url_template, username, server_hash, ip); 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, Ok(resp) => resp,
Err(err) => { Err(err) => {
tracing::warn!( tracing::warn!(
@ -136,7 +136,7 @@ pub fn authenticate(
} }
match status { match status {
StatusCode::OK => match response.body_mut().read_json::<GameProfile>() { StatusCode::OK => match response.json::<GameProfile>().await {
Ok(profile) => return Ok(profile), Ok(profile) => return Ok(profile),
Err(err) => { Err(err) => {
tracing::warn!("Failed to parse GameProfile response from '{address}': {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 = let textures: ProfileTextures =
serde_json::from_slice(&from64).map_err(|e| TextureError::JSONError(e.to_string()))?; serde_json::from_slice(&from64).map_err(|e| TextureError::JSONError(e.to_string()))?;
for texture in textures.textures { for texture in textures.textures {
let url = texture let url = Url::parse(&texture.1.url).map_err(|_| TextureError::InvalidURL)?;
.1
.url
.parse()
.map_err(|_| TextureError::InvalidURL)?;
is_texture_url_valid(&url, config)?; is_texture_url_valid(&url, config)?;
} }
Ok(()) Ok(())
} }
pub fn is_texture_url_valid(url: &Uri, config: &TextureConfig) -> Result<(), TextureError> { pub fn is_texture_url_valid(url: &Url, config: &TextureConfig) -> Result<(), TextureError> {
let Some(scheme) = url.scheme() else { let scheme = url.scheme();
return Err(TextureError::InvalidURL);
};
if !config if !config
.allowed_url_schemes .allowed_url_schemes
.iter() .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())); return Err(TextureError::DisallowedUrlScheme(scheme.to_string()));
} }
let Some(domain) = url.authority() else { let Some(domain) = url.domain() else {
return Err(TextureError::InvalidURL); return Err(TextureError::InvalidURL);
}; };
if !config if !config
.allowed_url_domains .allowed_url_domains
.iter() .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())); return Err(TextureError::DisallowedUrlDomain(domain.to_string()));
} }
Ok(()) Ok(())
} }
pub fn fetch_mojang_public_keys( pub async fn fetch_mojang_public_keys(
auth_config: &AuthenticationConfig, auth_config: &AuthenticationConfig,
) -> Result<Vec<RsaPublicKey>, AuthError> { ) -> Result<Vec<RsaPublicKey>, AuthError> {
let services_url = auth_config let services_url = auth_config
@ -211,10 +205,11 @@ pub fn fetch_mojang_public_keys(
let url = format!("{services_url}/publickeys"); let url = format!("{services_url}/publickeys");
let agent = create_agent(auth_config); let client = create_client(auth_config);
let mut response = agent let response = client
.get(&url) .get(&url)
.call() .send()
.await
.map_err(|_| AuthError::FailedResponse)?; .map_err(|_| AuthError::FailedResponse)?;
match response.status() { match response.status() {
@ -223,10 +218,8 @@ pub fn fetch_mojang_public_keys(
other => Err(AuthError::UnknownStatusCode(other))?, other => Err(AuthError::UnknownStatusCode(other))?,
} }
let public_keys: MojangPublicKeys = response let public_keys: MojangPublicKeys =
.body_mut() response.json().await.map_err(|_| AuthError::FailedParse)?;
.read_json()
.map_err(|_| AuthError::FailedParse)?;
let as_rsa_keys = public_keys let as_rsa_keys = public_keys
.player_certificate_keys .player_certificate_keys
@ -248,7 +241,7 @@ struct MojangProfileByNameResponse {
name: String, name: String,
} }
pub fn lookup_profile_by_name( pub async fn lookup_profile_by_name(
name: &str, name: &str,
auth_config: &AuthenticationConfig, auth_config: &AuthenticationConfig,
) -> Result<Option<(Uuid, String)>, AuthError> { ) -> Result<Option<(Uuid, String)>, AuthError> {
@ -263,7 +256,7 @@ pub fn lookup_profile_by_name(
candidate_urls.push(fallback.as_str()); 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 not_found_count = 0;
let mut last_unknown_status = None; let mut last_unknown_status = None;
@ -271,7 +264,7 @@ pub fn lookup_profile_by_name(
for url_template in candidate_urls { for url_template in candidate_urls {
let address = url_template.replace("{username}", name); 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, Ok(resp) => resp,
Err(err) => { Err(err) => {
tracing::warn!( tracing::warn!(
@ -288,10 +281,7 @@ pub fn lookup_profile_by_name(
} }
match status { match status {
StatusCode::OK => match response StatusCode::OK => match response.json::<MojangProfileByNameResponse>().await {
.body_mut()
.read_json::<MojangProfileByNameResponse>()
{
Ok(profile) => { Ok(profile) => {
let parsed_uuid = let parsed_uuid =
Uuid::parse_str(&profile.id).map_err(|_| AuthError::FailedParse)?; 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, uuid: Uuid,
auth_config: &AuthenticationConfig, auth_config: &AuthenticationConfig,
) -> Result<Option<GameProfile>, AuthError> { ) -> Result<Option<GameProfile>, AuthError> {
@ -336,7 +339,7 @@ pub fn fetch_profile_by_uuid(
candidate_urls.push(fallback.as_str()); 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 not_found_count = 0;
let mut last_unknown_status = None; let mut last_unknown_status = None;
@ -348,7 +351,7 @@ pub fn fetch_profile_by_uuid(
.replace("{uuid}", &uuid_simple) .replace("{uuid}", &uuid_simple)
.replace("{uuid_hyphenated}", &uuid.to_string()); .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, Ok(resp) => resp,
Err(err) => { Err(err) => {
tracing::warn!("Profile fetch server at '{address}' is down or unreachable: {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 { match status {
StatusCode::OK => match response.body_mut().read_json::<GameProfile>() { StatusCode::OK => match response.json::<GameProfile>().await {
Ok(profile) => return Ok(Some(profile)), Ok(profile) => return Ok(Some(profile)),
Err(err) => { Err(err) => {
tracing::warn!("Failed to parse GameProfile response from '{address}': {err}"); tracing::warn!("Failed to parse GameProfile response from '{address}': {err}");

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

View File

@ -66,7 +66,10 @@ impl PendingConnection {
}; };
if server.advanced_config.networking.java.online_mode { 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), Ok(new_profile) => self.gameprofile = Some(new_profile),
Err(error) => { Err(error) => {
self.kick(match error { self.kick(match error {
@ -168,7 +171,7 @@ impl PendingConnection {
Some(PacketHandlerResult::ReadyToPlay(profile.clone(), config)) Some(PacketHandlerResult::ReadyToPlay(profile.clone(), config))
} }
fn authenticate( async fn authenticate(
&self, &self,
server: &Server, server: &Server,
shared_secret: &[u8], shared_secret: &[u8],
@ -181,7 +184,8 @@ impl PendingConnection {
&hash, &hash,
&ip, &ip,
&server.advanced_config.networking.java.authentication, &server.advanced_config.networking.java.authentication,
)?; )
.await?;
if let Some(actions) = &profile.profile_actions { if let Some(actions) = &profile.profile_actions {
if server if server

View File

@ -4,10 +4,10 @@ use super::*;
impl JavaClient { impl JavaClient {
pub fn handle_chunk_batch(&self, player: &Player, packet: &SChunkBatch) { pub fn handle_chunk_batch(&self, player: &Player, packet: &SChunkBatch) {
player player
.chunk_manager .chunk_sender
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) .unwrap_or_else(std::sync::PoisonError::into_inner)
.handle_acknowledge(packet.chunks_per_tick); .on_batch_acknowledged(packet.chunks_per_tick);
trace!( trace!(
"Client requested {} chunks per tick", "Client requested {} chunks per tick",
packet.chunks_per_tick packet.chunks_per_tick

View File

@ -28,6 +28,8 @@ use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
pub mod authentication; pub mod authentication;
pub mod bedrock; pub mod bedrock;
pub mod chunk_sender;
pub use chunk_sender::ChunkSender;
pub mod java; pub mod java;
pub mod lan_broadcast; pub mod lan_broadcast;
pub mod packet_limiter; pub mod packet_limiter;

View File

@ -182,15 +182,43 @@ pub fn fetch_market_public_key() -> Result<String, String> {
return Ok(cached_key.clone()); return Ok(cached_key.clone());
} }
let mut response = ureq::get(PUMPKIN_MARKET_PUBLIC_KEY_URL) let body = if let Ok(handle) = tokio::runtime::Handle::try_current() {
.header("User-Agent", "Pumpkin-MC") tokio::task::block_in_place(|| {
.call() handle.block_on(async {
.map_err(|e| format!("Failed to fetch public key from market: {e}"))?; let client = reqwest::Client::builder()
.user_agent("Pumpkin-MC")
let body = response .build()
.body_mut() .map_err(|e| format!("Failed to build HTTP client: {e}"))?;
.read_to_string() let response = client
.map_err(|e| format!("Failed to read public key response: {e}"))?; .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}"))
})
})?
} 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(); let key = body.trim().trim_matches('"').to_string();
if key.is_empty() { if key.is_empty() {

View File

@ -1,6 +1,6 @@
use pumpkin_data::enchantment::AttributeModifierSlot; use pumpkin_data::enchantment::AttributeModifierSlot;
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use std::collections::HashMap; use rustc_hash::FxHashMap;
use tokio::sync::RwLock; use tokio::sync::RwLock;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -16,7 +16,7 @@ pub struct CustomEnchantmentEntry {
} }
pub struct EnchantmentManager { pub struct EnchantmentManager {
custom_enchantments: RwLock<HashMap<String, CustomEnchantmentEntry>>, custom_enchantments: RwLock<FxHashMap<String, CustomEnchantmentEntry>>,
} }
impl Default for EnchantmentManager { impl Default for EnchantmentManager {
@ -29,7 +29,7 @@ impl EnchantmentManager {
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
custom_enchantments: RwLock::new(HashMap::new()), custom_enchantments: RwLock::new(FxHashMap::default()),
} }
} }

View File

@ -43,7 +43,7 @@ use pumpkin_world::world_info::{LevelData, WorldInfoError, WorldInfoReader, Worl
use rand::seq::{IndexedRandom, SliceRandom}; use rand::seq::{IndexedRandom, SliceRandom};
use rayon::prelude::*; use rayon::prelude::*;
use rsa::RsaPublicKey; use rsa::RsaPublicKey;
use std::collections::HashSet; use rustc_hash::FxHashSet;
use std::fs; use std::fs;
use std::net::IpAddr; use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
@ -330,10 +330,12 @@ impl Server {
.java .java
.authentication .authentication
.clone(); .clone();
let keys = fetch_mojang_public_keys(&auth_config).unwrap_or_else(|e| { let keys = fetch_mojang_public_keys(&auth_config)
error!("Failed to fetch Mojang keys: {e}"); .await
Vec::new() .unwrap_or_else(|e| {
}); error!("Failed to fetch Mojang keys: {e}");
Vec::new()
});
server_clone.mojang_public_keys.store(Arc::new(keys)); server_clone.mojang_public_keys.store(Arc::new(keys));
}); });
} }
@ -358,7 +360,9 @@ impl Server {
auth.url.as_deref(), auth.url.as_deref(),
auth.connect_timeout, auth.connect_timeout,
auth.read_timeout, auth.read_timeout,
) { )
.await
{
Ok(keys) => keys, Ok(keys) => keys,
Err(error) => { Err(error) => {
error!("Failed to fetch Bedrock OIDC keys: {error}"); error!("Failed to fetch Bedrock OIDC keys: {error}");
@ -1179,7 +1183,7 @@ impl Server {
None None
} }
}) })
.collect::<HashSet<_>>(); .collect::<FxHashSet<_>>();
let type_excluded = target_selector let type_excluded = target_selector
.conditions .conditions
.iter() .iter()
@ -1190,7 +1194,7 @@ impl Server {
None None
} }
}) })
.collect::<HashSet<_>>(); .collect::<FxHashSet<_>>();
players.retain(|_| { players.retain(|_| {
(type_excluded.is_empty() || !type_excluded.contains(player_type)) (type_excluded.is_empty() || !type_excluded.contains(player_type))
@ -1290,7 +1294,7 @@ impl Server {
None None
} }
}) })
.collect::<HashSet<_>>(); .collect::<FxHashSet<_>>();
let type_excluded = target_selector let type_excluded = target_selector
.conditions .conditions
.iter() .iter()
@ -1301,7 +1305,7 @@ impl Server {
None None
} }
}) })
.collect::<HashSet<_>>(); .collect::<FxHashSet<_>>();
entities.retain(|entity| { entities.retain(|entity| {
// Filter by entity type // Filter by entity type
(type_excluded.is_empty() || !type_excluded.contains(&entity.get_entity().entity_type)) (type_excluded.is_empty() || !type_excluded.contains(&entity.get_entity().entity_type))

View File

@ -3,7 +3,7 @@ use std::{num::NonZero, sync::Arc};
use pumpkin_protocol::{ use pumpkin_protocol::{
bedrock::client::network_chunk_publisher_update::CNetworkChunkPublisherUpdate, bedrock::client::network_chunk_publisher_update::CNetworkChunkPublisherUpdate,
java::client::play::{CCenterChunk, CUnloadChunk}, java::client::play::CCenterChunk,
}; };
use pumpkin_world::cylindrical_chunk_iterator::Cylindrical; 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 loading_chunks: Vec<_> = loading_iter.collect();
let unloading_chunks: Vec<_> = unloading_iter.collect(); let unloading_chunks: Vec<_> = unloading_iter.collect();
// Use the chunk_manager's world reference, which is updated on dimension change. let world = player.world();
// This ensures we load chunks from the correct world after portal teleportation. let level = &world.level;
let world = { let mut held_tickets = player
let mut chunk_manager = player .held_chunk_tickets
.chunk_manager .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
{
let mut lock = level
.chunk_loading
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
let world = chunk_manager.world().clone(); let new_level = pumpkin_world::chunk_system::ChunkLoading::get_level_from_view_distance(
chunk_manager.update_center_and_view_distance( u8::from(view_distance) + 1,
new_chunk_center,
view_distance.into(),
&world.level,
&loading_chunks,
&unloading_chunks,
); );
world lock.add_ticket(new_chunk_center, new_level);
};
player.watched_section.store(new_cylindrical);
if let ClientPlatform::Java(client) = player.client.as_ref() { let sim_dist = world.server.upgrade().map_or(10, |s| {
for chunk in &unloading_chunks { s.advanced_config.networking.java.simulation_distance.get()
client.try_send_packet(&CUnloadChunk::new(chunk.x, chunk.y)); });
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 // 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. // 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() { if !loading_chunks.is_empty() {
world.spawn_world_entity_chunks(player.clone(), loading_chunks, new_chunk_center); world.spawn_world_entity_chunks(player.clone(), loading_chunks, new_chunk_center);
} }
world.entity_tracker.update_player_position(player, &world);
} }

View File

@ -3,7 +3,7 @@ use crate::entity::player::Player;
use crate::server::Server; use crate::server::Server;
use crate::world::bossbar::{Bossbar, BossbarColor, BossbarDivisions}; use crate::world::bossbar::{Bossbar, BossbarColor, BossbarDivisions};
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use std::collections::HashMap; use rustc_hash::FxHashMap;
use std::sync::Arc; use std::sync::Arc;
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
@ -43,7 +43,7 @@ impl CustomBossbar {
} }
pub struct CustomBossbars { pub struct CustomBossbars {
pub custom_bossbars: HashMap<String, CustomBossbar>, pub custom_bossbars: FxHashMap<String, CustomBossbar>,
} }
impl Default for CustomBossbars { impl Default for CustomBossbars {
@ -56,7 +56,7 @@ impl CustomBossbars {
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
custom_bossbars: HashMap::new(), custom_bossbars: FxHashMap::default(),
} }
} }

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

View File

@ -116,7 +116,7 @@ use pumpkin_protocol::{
java::{ java::{
self, self,
client::play::{ client::play::{
CBlockEntityData, CEntityStatus, CGameEvent, CLogin, CMultiBlockUpdate, CBlockEntityData, CDamageEvent, CEntityStatus, CGameEvent, CLogin, CMultiBlockUpdate,
CPlayerChatMessage, CPlayerInfoUpdate, CRemoveEntities, CRemovePlayerInfo, CPlayerChatMessage, CPlayerInfoUpdate, CRemoveEntities, CRemovePlayerInfo,
CSetSelectedSlot, CSoundEffect, CSpawnEntity, FilterType, GameEvent, InitChat, CSetSelectedSlot, CSoundEffect, CSpawnEntity, FilterType, GameEvent, InitChat,
PlayerAction, PlayerInfoFlags, PlayerAction, PlayerInfoFlags,
@ -160,6 +160,7 @@ pub mod bossbar;
pub mod custom_bossbar; pub mod custom_bossbar;
pub mod dragon_fight; pub mod dragon_fight;
pub mod end_podium; pub mod end_podium;
pub mod entity_tracker;
pub mod natural_spawner; pub mod natural_spawner;
pub mod scoreboard; pub mod scoreboard;
pub mod weather; pub mod weather;
@ -280,6 +281,8 @@ pub struct World {
pub custom_data: std::sync::Mutex<NbtCompound>, pub custom_data: std::sync::Mutex<NbtCompound>,
/// Persistent custom data for block entities at specific positions /// Persistent custom data for block entities at specific positions
pub custom_block_entity_data: DashMap<BlockPos, NbtCompound>, 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)] #[derive(Clone, Copy)]
@ -403,6 +406,7 @@ impl World {
block_entities: DashMap::new(), block_entities: DashMap::new(),
custom_data: std::sync::Mutex::new(custom_data), custom_data: std::sync::Mutex::new(custom_data),
custom_block_entity_data: DashMap::new(), 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. /// Broadcasts an entity status update / event to all players tracking the specified entity,
pub fn send_entity_status( /// and to the entity itself if it is a player.
/// Matching Vanilla's `ServerLevel.broadcastEntityEvent(entity, event)`.
pub fn broadcast_entity_event(
&self, &self,
entity: &Entity, entity: &Entity,
java_status: EntityStatus, java_status: EntityStatus,
bedrock_status: Option<ActorEventID>, bedrock_status: Option<ActorEventID>,
) { ) {
let chunk_pos = entity.chunk_pos.load();
let je_packet = CEntityStatus::new(entity.entity_id, java_status as i8); let je_packet = CEntityStatus::new(entity.entity_id, java_status as i8);
if let Some(be_event) = bedrock_status { if let Some(be_event) = bedrock_status {
let be_packet = SActorEvent { let be_packet = SActorEvent {
@ -562,14 +567,44 @@ impl World {
data: VarInt(0), data: VarInt(0),
fire_at_position: None, 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 { } 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) { pub fn send_remove_mob_effect(&self, entity: &Entity, effect_type: &'static StatusEffect) {
let chunk_pos = entity.chunk_pos.load();
let je_packet = let je_packet =
CRemoveMobEffect::new(entity.entity_id.into(), VarInt(i32::from(effect_type.id))); CRemoveMobEffect::new(entity.entity_id.into(), VarInt(i32::from(effect_type.id)));
@ -583,11 +618,10 @@ impl World {
tick: VarULong(0), tick: VarULong(0),
ambient: false, 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) { 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; let mut flags: i8 = 0;
if effect.ambient { if effect.ambient {
flags |= 0x01; flags |= 0x01;
@ -618,7 +652,91 @@ impl World {
ambient: effect.ambient, 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) { pub fn set_difficulty(&self, difficulty: Difficulty) {
@ -1323,6 +1441,8 @@ impl World {
}); });
let entity_elapsed = t_entities.elapsed(); let entity_elapsed = t_entities.elapsed();
self.entity_tracker.update_all(self);
let mut block_entities: Vec<Arc<dyn BlockEntity>> = Vec::new(); let mut block_entities: Vec<Arc<dyn BlockEntity>> = Vec::new();
for chunk_pos in active_chunks.iter() { for chunk_pos in active_chunks.iter() {
if let Some(chunk_block_entities) = self.block_entities.get(chunk_pos) { if let Some(chunk_block_entities) = self.block_entities.get(chunk_pos) {
@ -3788,13 +3908,7 @@ impl World {
container_id: 0, container_id: 0,
}; };
let chunk_pos = from.get_entity().chunk_pos.load(); self.send_to_tracking_players_editioned(from.get_entity(), &je_packet, &be_mob_equipment);
self.broadcast_to_chunk_except_editioned(
chunk_pos,
&[from.get_entity().entity_uuid],
&je_packet,
&be_mob_equipment,
);
} }
pub fn send_world_info( pub fn send_world_info(
@ -4095,11 +4209,7 @@ impl World {
// observer sees the player in a world whose chunk manager doesn't match. // observer sees the player in a world whose chunk manager doesn't match.
self.remove_player(player, false).await; self.remove_player(player, false).await;
player.unload_watched_chunks(self).await; player.unload_watched_chunks(self).await;
player player.change_world_chunks(&self.level, &destination);
.chunk_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.change_world(&self.level, destination.clone());
player.living_entity.entity.set_world(destination.clone()); player.living_entity.entity.set_world(destination.clone());
destination.players.rcu(|current_list| { destination.players.rcu(|current_list| {
let mut new_list = (**current_list).clone(); let mut new_list = (**current_list).clone();
@ -4710,6 +4820,8 @@ impl World {
new_list.push(player.clone()); new_list.push(player.clone());
new_list new_list
}); });
self.entity_tracker
.add_entity(&(player.clone() as Arc<dyn EntityBase>), self);
Ok(()) Ok(())
} }
@ -4751,6 +4863,8 @@ impl World {
new_list new_list
}); });
if let Some(ref player) = removed_player { 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 uuid = player.gameprofile.id;
let entity_id = player.entity_id(); let entity_id = player.entity_id();
@ -4805,7 +4919,7 @@ impl World {
#[expect(clippy::needless_pass_by_value)] #[expect(clippy::needless_pass_by_value)]
pub fn spawn_entity_non_save(&self, entity: Arc<dyn EntityBase>) { pub fn spawn_entity_non_save(&self, entity: Arc<dyn EntityBase>) {
let _base_entity = entity.get_entity(); 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.spawn_state.load().add_entity(self, entity.as_ref());
self.entities.rcu(|current_entities| { self.entities.rcu(|current_entities| {
@ -4829,7 +4943,6 @@ impl World {
return; return;
} }
self.broadcast_entity_spawn(&entity);
entity.init_data_tracker(); entity.init_data_tracker();
self.add_entity_silent(entity); 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 // 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). // serialized at once (which would double it on the next reload).
self.spawn_state.load().add_entity(self, entity.as_ref()); self.spawn_state.load().add_entity(self, entity.as_ref());
self.entity_tracker.add_entity(&entity, self);
self.entities.rcu(|current_entities| { self.entities.rcu(|current_entities| {
let mut new_entities = (**current_entities).clone(); let mut new_entities = (**current_entities).clone();
@ -4889,18 +5003,12 @@ impl World {
base_entity.removed.store(true, Ordering::Release); base_entity.removed.store(true, Ordering::Release);
self.spawn_state.load().remove_entity(self, entity); self.spawn_state.load().remove_entity(self, entity);
self.entity_tracker.remove_entity(entity, self);
self.entities.rcu(|current_entities| { self.entities.rcu(|current_entities| {
let mut new_entities = (**current_entities).clone(); let mut new_entities = (**current_entities).clone();
new_entities.retain(|e| e.get_entity().entity_uuid != base_entity.entity_uuid); new_entities.retain(|e| e.get_entity().entity_uuid != base_entity.entity_uuid);
new_entities 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( pub async fn remove_entities_in_chunks(
@ -4929,6 +5037,7 @@ impl World {
}); });
for entity in entities_to_remove { for entity in entities_to_remove {
self.entity_tracker.remove_entity(entity.as_ref(), self);
self.save_entity(&entity).await; self.save_entity(&entity).await;
self.spawn_state.load().remove_entity(self, entity.as_ref()); self.spawn_state.load().remove_entity(self, entity.as_ref());
} }

View File

@ -11,7 +11,6 @@ use pumpkin_data::tag::Taggable;
use pumpkin_data::tag::WorldgenBiome::MINECRAFT_REDUCE_WATER_AMBIENT_SPAWNS; use pumpkin_data::tag::WorldgenBiome::MINECRAFT_REDUCE_WATER_AMBIENT_SPAWNS;
use pumpkin_data::{Block, BlockDirection, BlockState}; use pumpkin_data::{Block, BlockDirection, BlockState};
use pumpkin_util::GameMode; use pumpkin_util::GameMode;
use pumpkin_util::math::boundingbox::{BoundingBox, EntityDimensions};
use pumpkin_util::math::get_section_cord; use pumpkin_util::math::get_section_cord;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector2::Vector2; 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) { if !check_spawn_rules(entity_type, world, block_pos, is_thundering) {
return false; return false;
} }
// TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes if !world.is_space_empty(entity_type.get_spawn_bounding_box(
if !world.is_space_empty(BoundingBox::new_from_pos(
f64::from(block_pos.0.x) + 0.5, f64::from(block_pos.0.x) + 0.5,
f64::from(block_pos.0.y), f64::from(block_pos.0.y),
f64::from(block_pos.0.z) + 0.5, 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; return false;
} }

View File

@ -1,4 +1,4 @@
use std::collections::HashMap; use rustc_hash::FxHashMap;
use pumpkin_data::scoreboard::ScoreboardDisplaySlot; use pumpkin_data::scoreboard::ScoreboardDisplaySlot;
use pumpkin_protocol::{ use pumpkin_protocol::{
@ -114,10 +114,10 @@ impl ScoreboardTarget for NoTarget {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Scoreboard { pub struct Scoreboard {
objectives: HashMap<String, ScoreboardObjective>, objectives: FxHashMap<String, ScoreboardObjective>,
display_slots: HashMap<ScoreboardDisplaySlot, String>, display_slots: FxHashMap<ScoreboardDisplaySlot, String>,
scores: HashMap<String, HashMap<String, ScoreboardScore>>, scores: FxHashMap<String, FxHashMap<String, ScoreboardScore>>,
teams: HashMap<String, Team>, teams: FxHashMap<String, Team>,
} }
impl Scoreboard { impl Scoreboard {
@ -127,7 +127,7 @@ impl Scoreboard {
} }
#[must_use] #[must_use]
pub const fn get_objectives(&self) -> &HashMap<String, ScoreboardObjective> { pub const fn get_objectives(&self) -> &FxHashMap<String, ScoreboardObjective> {
&self.objectives &self.objectives
} }
@ -141,7 +141,7 @@ impl Scoreboard {
} }
#[must_use] #[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 &self.display_slots
} }
@ -151,7 +151,7 @@ impl Scoreboard {
} }
#[must_use] #[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 &self.scores
} }
@ -170,13 +170,13 @@ impl Scoreboard {
pub fn get_scores_for_objective( pub fn get_scores_for_objective(
&self, &self,
objective_name: &str, objective_name: &str,
) -> Option<&HashMap<String, ScoreboardScore>> { ) -> Option<&FxHashMap<String, ScoreboardScore>> {
self.scores.get(objective_name) self.scores.get(objective_name)
} }
#[must_use] #[must_use]
pub fn get_scores_for_entity(&self, entity_name: &str) -> HashMap<String, &ScoreboardScore> { pub fn get_scores_for_entity(&self, entity_name: &str) -> FxHashMap<String, &ScoreboardScore> {
let mut entity_scores = HashMap::new(); let mut entity_scores = FxHashMap::default();
for (obj_name, obj_scores) in &self.scores { for (obj_name, obj_scores) in &self.scores {
if let Some(score) = obj_scores.get(entity_name) { if let Some(score) = obj_scores.get(entity_name) {
entity_scores.insert(obj_name.clone(), score); entity_scores.insert(obj_name.clone(), score);
@ -186,7 +186,7 @@ impl Scoreboard {
} }
#[must_use] #[must_use]
pub const fn get_teams(&self) -> &HashMap<String, Team> { pub const fn get_teams(&self) -> &FxHashMap<String, Team> {
&self.teams &self.teams
} }
@ -859,9 +859,9 @@ pub struct BedrockObjective {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct BedrockScoreboard { pub struct BedrockScoreboard {
pub objectives: HashMap<String, BedrockObjective>, pub objectives: FxHashMap<String, BedrockObjective>,
pub display_slots: HashMap<BedrockDisplaySlot, String>, pub display_slots: FxHashMap<BedrockDisplaySlot, String>,
pub scores: HashMap<(String, String), i32>, pub scores: FxHashMap<(String, String), i32>,
} }
impl BedrockScoreboard { impl BedrockScoreboard {

View File

@ -36,10 +36,18 @@ pub struct EntityType {
pub category: MobCategory, pub category: MobCategory,
/// Whether this entity can spawn far from the player (beyond normal spawn range). /// Whether this entity can spawn far from the player (beyond normal spawn range).
pub can_spawn_far_from_player: bool, 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. /// Bounding box dimensions as `[width, height]` in blocks.
pub dimension: [f32; 2], pub dimension: [f32; 2],
/// Eye height in blocks, used for line-of-sight calculations. /// Eye height in blocks, used for line-of-sight calculations.
pub eye_height: f32, 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. /// Spawn location restrictions for natural spawning.
pub spawn_restriction: SpawnRestriction, pub spawn_restriction: SpawnRestriction,
} }
@ -173,6 +181,7 @@ impl ToTokens for NamedEntityType<'_> {
let dimension0 = entity.dimension[0]; let dimension0 = entity.dimension[0];
let dimension1 = entity.dimension[1]; let dimension1 = entity.dimension[1];
let spawn_dimensions_scale = entity.spawn_dimensions_scale;
let loot_table = if let Some(table) = &entity.loot_table { let loot_table = if let Some(table) = &entity.loot_table {
let table_tokens = table.to_token_stream(); 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 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! { tokens.extend(quote! {
EntityType { EntityType {
@ -197,9 +209,13 @@ impl ToTokens for NamedEntityType<'_> {
fire_immune: #fire_immune, fire_immune: #fire_immune,
category: &#spawn_category, category: &#spawn_category,
can_spawn_far_from_player: #can_spawn_far_from_player, 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, loot_table: #loot_table,
dimension: [#dimension0, #dimension1], // Correctly construct the array dimension: [#dimension0, #dimension1], // Correctly construct the array
eye_height: #eye_height, eye_height: #eye_height,
spawn_dimensions_scale: #spawn_dimensions_scale,
spawn_restriction: #spawn_restriction, spawn_restriction: #spawn_restriction,
resource_name: #name, resource_name: #name,
} }
@ -250,6 +266,8 @@ pub fn build() -> TokenStream {
use crate::sound::Sound; use crate::sound::Sound;
use pumpkin_util::loot_table::*; use pumpkin_util::loot_table::*;
use pumpkin_util::HeightMap; use pumpkin_util::HeightMap;
use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::vector3::Vector3;
use std::hash::Hash; use std::hash::Hash;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -266,9 +284,13 @@ pub fn build() -> TokenStream {
pub fire_immune: bool, pub fire_immune: bool,
pub category: &'static MobCategory, pub category: &'static MobCategory,
pub can_spawn_far_from_player: bool, 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 loot_table: Option<LootTable>,
pub dimension: [f32; 2], pub dimension: [f32; 2],
pub eye_height: f32, pub eye_height: f32,
pub spawn_dimensions_scale: f32,
pub spawn_restriction: SpawnRestriction, pub spawn_restriction: SpawnRestriction,
pub resource_name: &'static str, pub resource_name: &'static str,
} }
@ -423,6 +445,31 @@ pub fn build() -> TokenStream {
_ => None _ => 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 { impl IDSetContent for EntityType {