diff --git a/Cargo.lock b/Cargo.lock index 2cfd328a6..b10a0c89f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -263,9 +263,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.44" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "515a1f282e33d55983c499d7e9e87082e81cbc32974825bf9032f928392d5844" +checksum = "4f10dafd0c8d2e51ae9a748805777613ed0bbe17bf586b76c8311f45c020a32f" dependencies = [ "compression-codecs", "compression-core", @@ -398,9 +398,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block-buffer" @@ -746,9 +746,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe67f2944eef52fc7b106b8c9450d243a88701a0c065f7f57235e76abaed7df" +checksum = "58a6d0db8759036a783bc7c3f7a07f8cef3bf9470eb1db3bc86e8bcd1c5d0fe8" dependencies = [ "compression-core", "flate2", @@ -2242,7 +2242,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "inotify-sys", "libc", ] @@ -2299,9 +2299,9 @@ checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "is_terminal_polyfill" @@ -2403,9 +2403,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", @@ -2428,7 +2428,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", ] @@ -2665,7 +2665,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -2697,7 +2697,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "fsevent-sys", "inotify", "kqueue", @@ -2715,7 +2715,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -2806,7 +2806,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -3110,7 +3110,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crc32fast", "fdeflate", "flate2", @@ -3335,7 +3335,7 @@ dependencies = [ "async-trait", "axum", "base64 0.23.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "colored", "console-subscriber", @@ -3606,7 +3606,7 @@ dependencies = [ "aes 0.9.3", "async-compression", "base64 0.23.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "cesu8", "cfb8", @@ -3650,7 +3650,7 @@ name = "pumpkin-world" version = "0.1.0+26.2-26.45" dependencies = [ "arc-swap", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "criterion", "crossbeam", @@ -3789,7 +3789,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -3858,11 +3858,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", "futures-channel", "futures-core", @@ -4299,7 +4299,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -4318,9 +4318,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" dependencies = [ "log", "once_cell", @@ -4402,7 +4402,7 @@ version = "18.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "clipboard-win", "libc", @@ -4485,7 +4485,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation", "core-foundation-sys", "libc", @@ -5173,7 +5173,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-util", "http", @@ -5396,9 +5396,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -5409,9 +5409,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.77" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ "js-sys", "wasm-bindgen", @@ -5419,9 +5419,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5429,22 +5429,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] @@ -5477,7 +5477,7 @@ version = "0.258.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "hashbrown 0.17.1", "indexmap", "semver", @@ -5502,7 +5502,7 @@ source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436 dependencies = [ "addr2line", "async-trait", - "bitflags 2.13.1", + "bitflags 2.13.2", "bumpalo", "bytes", "cc", @@ -5699,7 +5699,7 @@ version = "49.0.0-dev" source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" dependencies = [ "anyhow", - "bitflags 2.13.1", + "bitflags 2.13.2", "heck", "indexmap", "wit-component", @@ -5712,7 +5712,7 @@ version = "49.0.0-dev" source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" dependencies = [ "async-trait", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "cap-primitives", "futures", @@ -5767,9 +5767,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -6129,7 +6129,7 @@ version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "windows-sys 0.59.0", ] @@ -6191,7 +6191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "481b5c47b2ecce0389b5e08a05557d6a190c9cd761773b8880a8017ee04dc7ef" dependencies = [ "anyhow", - "bitflags 2.13.1", + "bitflags 2.13.2", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 2db502445..fffda712b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,9 +149,9 @@ serde_json = { version = "1.0", default-features = false, features = ["std"] } tempfile = { version = "3.27.0", default-features = false } proc-macro-error2 = { version = "2", default-features = false } aes = { version = "0.9", default-features = false } -async-compression = { version = "0.4.44", default-features = false } +async-compression = { version = "0.4.46", default-features = false } base64 = { version = "0.23.1", default-features = false, features = ["std"] } -bitflags = { version = "2.13.1", default-features = false, features = ["std"] } +bitflags = { version = "2.13.2", default-features = false, features = ["std"] } cesu8 = { version = "1.1", default-features = false } cfb8 = { version = "0.9", default-features = false } colored = { version = "3.1", default-features = false } @@ -215,7 +215,7 @@ time = { version = "0.3", default-features = false, features = ["std"] } arc-swap = { version = "1.9", default-features = false } tokio-util = { version = "0.7.19", default-features = false } toml = { version = "1.1", default-features = false, features = ["parse", "display", "serde"] } -reqwest = { version = "0.13.4", default-features = false, features = ["rustls-no-provider", "json", "http2"] } +reqwest = { version = "0.13.5", default-features = false, features = ["rustls-no-provider", "json", "http2"] } notify = { version = "8.2.0", default-features = false, features = ["macos_fsevent"] } sysinfo = { version = "0.39", default-features = false, features = ["system"] } diff --git a/crates/pumpkin-gametest/src/model.rs b/crates/pumpkin-gametest/src/model.rs index 19346d837..df100dee7 100644 --- a/crates/pumpkin-gametest/src/model.rs +++ b/crates/pumpkin-gametest/src/model.rs @@ -6,6 +6,18 @@ use serde_json::Value; pub enum TestType { #[serde(rename = "minecraft:block_based")] BlockBased, + #[serde(rename = "minecraft:function")] + Function, +} + +impl TestType { + #[must_use] + pub const fn serialized_name(self) -> &'static str { + match self { + Self::BlockBased => "minecraft:block_based", + Self::Function => "minecraft:function", + } + } } #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] @@ -71,6 +83,8 @@ pub struct GameTestDefinition { pub instance_type: TestType, pub environment: Value, pub structure: String, + #[serde(default)] + pub function: Option, pub max_ticks: i32, #[serde(default)] pub setup_ticks: i32, @@ -93,7 +107,13 @@ pub struct GameTestDefinition { impl GameTestDefinition { #[must_use] pub fn is_valid(&self) -> bool { - self.max_ticks > 0 + let function_valid = match self.instance_type { + TestType::BlockBased => true, + TestType::Function => self.function.as_ref().is_some_and(|f| !f.is_empty()), + }; + + function_valid + && self.max_ticks > 0 && self.setup_ticks >= 0 && self.max_attempts > 0 && self.required_successes > 0 diff --git a/crates/pumpkin-protocol/src/java/client/config/feature_flags.rs b/crates/pumpkin-protocol/src/java/client/config/feature_flags.rs index b2ba8e3fe..e34ed8535 100644 --- a/crates/pumpkin-protocol/src/java/client/config/feature_flags.rs +++ b/crates/pumpkin-protocol/src/java/client/config/feature_flags.rs @@ -1,17 +1,20 @@ use pumpkin_data::packet::clientbound::config::UPDATE_ENABLED_FEATURES; use pumpkin_macros::java_packet; -use pumpkin_util::{resource_location::ResourceLocation, version::JavaMinecraftVersion}; +use pumpkin_util::version::JavaMinecraftVersion; use crate::{ClientPacket, ser::NetworkWriteExt}; #[java_packet(UPDATE_ENABLED_FEATURES)] pub struct CFeatureFlags<'a> { - pub features: &'a [ResourceLocation], + pub features: &'a [&'a str], } +pub type CUpdateEnabledFeatures<'a> = CFeatureFlags<'a>; +pub type ClientboundUpdateEnabledFeaturesPacket<'a> = CFeatureFlags<'a>; + impl<'a> CFeatureFlags<'a> { #[must_use] - pub const fn new(features: &'a [ResourceLocation]) -> Self { + pub const fn new(features: &'a [&'a str]) -> Self { Self { features } } } diff --git a/crates/pumpkin/src/data/datapack/mod.rs b/crates/pumpkin/src/data/datapack/mod.rs index 6b481a50e..68d055a3b 100644 --- a/crates/pumpkin/src/data/datapack/mod.rs +++ b/crates/pumpkin/src/data/datapack/mod.rs @@ -20,6 +20,13 @@ use self::test_loader::{ TestInstance, TestInstanceRegistry, load_test_instances_from_dir, to_registry_entry, }; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct KnownPackData { + pub namespace: String, + pub id: String, + pub version: String, +} + #[derive(Clone, Debug)] pub struct LoadedDatapack { pub id: String, @@ -29,6 +36,7 @@ pub struct LoadedDatapack { pub root_path: PathBuf, pub recipe_count: usize, pub function_count: usize, + pub known_packs: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -121,7 +129,7 @@ impl DatapackManager { continue; } - let (description, pack_format) = read_pack_mcmeta(&pack_path); + let (description, pack_format, known_packs) = read_pack_mcmeta(&pack_path); let (pack_recipe_count, pack_function_count, pack_test_instance_count) = load_pack_contents( @@ -144,6 +152,7 @@ impl DatapackManager { root_path: pack_path, recipe_count: pack_recipe_count, function_count: pack_function_count, + known_packs, }); } } @@ -226,6 +235,119 @@ impl DatapackManager { entries } + /// Returns the list of known packs for client configuration / pack synchronization, + /// matching vanilla's `server.getResourceManager().listPacks().flatMap(p -> p.location().knownPackInfo().stream())`. + pub fn get_known_packs<'a>( + &self, + server: &Server, + server_version: &'a str, + loaded_packs: &'a [LoadedDatapack], + ) -> Vec> { + use pumpkin_protocol::KnownPack; + + let mut known_packs = Vec::new(); + + // 1. Primary vanilla core pack + known_packs.push(KnownPack { + namespace: "minecraft", + id: "core", + version: server_version, + }); + + // 2. Built-in feature packs that are enabled + let enabled_packs = Self::get_enabled_packs(server); + for pack_name in &enabled_packs { + let id: &'static str = match pack_name.as_str() { + "trade_rebalance" => "trade_rebalance", + "minecart_improvements" => "minecart_improvements", + "redstone_experiments" => "redstone_experiments", + "bundle" => "bundle", + _ => continue, + }; + let pack = KnownPack { + namespace: "minecraft", + id, + version: server_version, + }; + if !known_packs + .iter() + .any(|p| p.namespace == pack.namespace && p.id == pack.id) + { + known_packs.push(pack); + } + } + + // 3. Loaded packs with known_pack info from pack.mcmeta + for pack in loaded_packs { + for kp in &pack.known_packs { + let p = KnownPack { + namespace: &kp.namespace, + id: &kp.id, + version: &kp.version, + }; + if !known_packs.iter().any(|existing| { + existing.namespace == p.namespace + && existing.id == p.id + && existing.version == p.version + }) { + known_packs.push(p); + } + } + } + + known_packs + } + + /// Returns the enabled world feature flags (e.g. `minecraft:vanilla`, `minecraft:trade_rebalance`, + /// `minecraft:minecart_improvements`, `minecraft:redstone_experiments`, `minecraft:bundle`). + #[must_use] + pub fn get_enabled_features(&self, server: &Server) -> Vec<&'static str> { + let enabled_packs = Self::get_enabled_packs(server); + Self::resolve_enabled_features(&enabled_packs) + } + + /// Resolves enabled feature flag names given a list of enabled pack names. + #[must_use] + pub fn resolve_enabled_features(enabled_packs: &[String]) -> Vec<&'static str> { + let mut features = vec!["minecraft:vanilla"]; + + for pack_name in enabled_packs { + let feature: &'static str = match pack_name.as_str() { + "trade_rebalance" | "file/trade_rebalance" => "minecraft:trade_rebalance", + "minecart_improvements" | "file/minecart_improvements" => { + "minecraft:minecart_improvements" + } + "redstone_experiments" | "file/redstone_experiments" => { + "minecraft:redstone_experiments" + } + "bundle" | "file/bundle" => "minecraft:bundle", + _ => { + if let Some(stripped) = pack_name.strip_prefix("file/") { + match stripped { + "trade_rebalance" => "minecraft:trade_rebalance", + "minecart_improvements" => "minecraft:minecart_improvements", + "redstone_experiments" => "minecraft:redstone_experiments", + "bundle" => "minecraft:bundle", + _ => continue, + } + } else { + continue; + } + } + }; + if !features.contains(&feature) { + features.push(feature); + } + } + + features + } + + #[must_use] + pub fn is_feature_enabled(&self, server: &Server, feature: &str) -> bool { + self.get_enabled_features(server).contains(&feature) + } + /// Loads a Java Edition structure NBT from the currently enabled datapacks. /// /// Structure identifiers are resource locations such as @@ -520,7 +642,7 @@ impl DatapackManager { .get_world_path() .join("datapacks") .join(stripped); - let (desc, format) = read_pack_mcmeta(&pack_path); + let (desc, format, _) = read_pack_mcmeta(&pack_path); (resolved_name.clone(), stripped.to_string(), desc, format) } else { ( @@ -821,7 +943,7 @@ fn load_pack_contents( ) } -fn read_pack_mcmeta(pack_path: &Path) -> (String, u32) { +fn read_pack_mcmeta(pack_path: &Path) -> (String, u32, Vec) { let mcmeta_path = pack_path.join("pack.mcmeta"); if let Ok(content) = fs::read_to_string(mcmeta_path) && let Ok(val) = serde_json::from_str::(&content) @@ -838,9 +960,49 @@ fn read_pack_mcmeta(pack_path: &Path) -> (String, u32) { .and_then(|p| p.get("pack_format")) .and_then(serde_json::Value::as_u64) .unwrap_or(61) as u32; - return (description, pack_format); + + let mut known_packs = Vec::new(); + let parse_item = |item: &serde_json::Value| -> Option { + let ns = item.get("namespace").and_then(serde_json::Value::as_str)?; + let id = item.get("id").and_then(serde_json::Value::as_str)?; + let ver = item.get("version").and_then(serde_json::Value::as_str)?; + Some(KnownPackData { + namespace: ns.to_string(), + id: id.to_string(), + version: ver.to_string(), + }) + }; + + if let Some(packs_array) = val.get("known_packs").and_then(serde_json::Value::as_array) { + for item in packs_array { + if let Some(kp) = parse_item(item) { + known_packs.push(kp); + } + } + } else if let Some(item) = val.get("known_pack") { + if let Some(kp) = parse_item(item) { + known_packs.push(kp); + } + } else if let Some(pack_obj) = pack { + if let Some(packs_array) = pack_obj + .get("known_packs") + .and_then(serde_json::Value::as_array) + { + for item in packs_array { + if let Some(kp) = parse_item(item) { + known_packs.push(kp); + } + } + } else if let Some(item) = pack_obj.get("known_pack") + && let Some(kp) = parse_item(item) + { + known_packs.push(kp); + } + } + + return (description, pack_format, known_packs); } - (String::new(), 61) + (String::new(), 61, Vec::new()) } fn load_recipes_from_dir( @@ -946,4 +1108,87 @@ mod tests { "Unknown function tag: #test:missing" ); } + + #[test] + fn read_pack_mcmeta_parses_known_packs() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let pack_path = temp_dir.path(); + + // 1. Test root known_packs array + let mcmeta_content = r#"{ + "pack": { + "description": "Test Pack", + "pack_format": 61 + }, + "known_packs": [ + { + "namespace": "example", + "id": "content", + "version": "1.0.0" + } + ] + }"#; + std::fs::write(pack_path.join("pack.mcmeta"), mcmeta_content).unwrap(); + + let (desc, format, known_packs) = super::read_pack_mcmeta(pack_path); + assert_eq!(desc, "Test Pack"); + assert_eq!(format, 61); + assert_eq!(known_packs.len(), 1); + assert_eq!(known_packs[0].namespace, "example"); + assert_eq!(known_packs[0].id, "content"); + assert_eq!(known_packs[0].version, "1.0.0"); + + // 2. Test nested pack.known_packs + let mcmeta_content_nested = r#"{ + "pack": { + "description": "Nested Pack", + "pack_format": 61, + "known_packs": [ + { + "namespace": "nested", + "id": "pack", + "version": "2.0.0" + } + ] + } + }"#; + std::fs::write(pack_path.join("pack.mcmeta"), mcmeta_content_nested).unwrap(); + + let (_, _, known_packs) = super::read_pack_mcmeta(pack_path); + assert_eq!(known_packs.len(), 1); + assert_eq!(known_packs[0].namespace, "nested"); + assert_eq!(known_packs[0].id, "pack"); + assert_eq!(known_packs[0].version, "2.0.0"); + } + + #[test] + fn resolve_enabled_features_works() { + // Default only vanilla + let default_features = DatapackManager::resolve_enabled_features(&[]); + assert_eq!(default_features, ["minecraft:vanilla"]); + + // With minecart improvements + let minecart_features = + DatapackManager::resolve_enabled_features(&["file/minecart_improvements".to_string()]); + assert_eq!( + minecart_features, + ["minecraft:vanilla", "minecraft:minecart_improvements"] + ); + + // Multiple experimental features + let multi_features = DatapackManager::resolve_enabled_features(&[ + "trade_rebalance".to_string(), + "redstone_experiments".to_string(), + "file/bundle".to_string(), + ]); + assert_eq!( + multi_features, + [ + "minecraft:vanilla", + "minecraft:trade_rebalance", + "minecraft:redstone_experiments", + "minecraft:bundle" + ] + ); + } } diff --git a/crates/pumpkin/src/data/datapack/test_loader.rs b/crates/pumpkin/src/data/datapack/test_loader.rs index 6bf6930ec..444c5f0bb 100644 --- a/crates/pumpkin/src/data/datapack/test_loader.rs +++ b/crates/pumpkin/src/data/datapack/test_loader.rs @@ -96,14 +96,11 @@ fn load_test_instances_recursive( #[must_use] pub fn to_registry_entry(entry_id: String, instance: &TestInstance) -> RegistryEntryData { let mut nbt = NbtCompound::new(); - nbt.put_string( - "type", - match instance.instance_type { - TestType::BlockBased => "minecraft:block_based", - } - .to_string(), - ); + nbt.put_string("type", instance.instance_type.serialized_name().to_string()); nbt.put("environment", json_value_to_nbt(&instance.environment)); + if let Some(function) = &instance.function { + nbt.put_string("function", function.clone()); + } nbt.put_string("structure", instance.structure.clone()); nbt.put_int("max_ticks", instance.max_ticks); nbt.put_int("setup_ticks", instance.setup_ticks); @@ -203,4 +200,66 @@ mod tests { assert!(!instance.sky_access); assert_eq!(instance.padding, 0); } + + #[test] + fn loads_function_test_instance_with_vanilla_defaults() { + let dir = tempfile::tempdir().expect("temp dir"); + + fs::write( + dir.path().join("always_pass.json"), + r#"{ + "type": "minecraft:function", + "environment": "minecraft:default", + "function": "minecraft:always_pass", + "max_ticks": 1, + "required": false, + "setup_ticks": 1, + "structure": "minecraft:empty" + }"#, + ) + .expect("write test instance"); + + let mut registry = TestInstanceRegistry::new(); + let loaded = load_test_instances_from_dir("minecraft", dir.path(), &mut registry); + + assert_eq!(loaded, 1); + + let instance = registry + .get("minecraft:always_pass") + .expect("test instance should be registered"); + + assert_eq!(instance.instance_type, TestType::Function); + assert_eq!( + instance.environment, + Value::String("minecraft:default".into()) + ); + assert_eq!(instance.function.as_deref(), Some("minecraft:always_pass")); + assert_eq!(instance.structure, "minecraft:empty"); + assert_eq!(instance.max_ticks, 1); + assert_eq!(instance.setup_ticks, 1); + assert!(!instance.required); + assert_eq!(instance.rotation, GameTestRotation::None); + assert!(!instance.manual_only); + assert_eq!(instance.max_attempts, 1); + assert_eq!(instance.required_successes, 1); + assert!(!instance.sky_access); + assert_eq!(instance.padding, 0); + } + + #[test] + fn loads_embedded_test_instances_including_always_pass() { + let mut registry = TestInstanceRegistry::new(); + let count = load_embedded_test_instances(&mut registry); + assert!(count > 0); + + let always_pass = registry + .get("minecraft:always_pass") + .expect("minecraft:always_pass should be successfully parsed and registered"); + assert_eq!(always_pass.instance_type, TestType::Function); + assert_eq!( + always_pass.function.as_deref(), + Some("minecraft:always_pass") + ); + assert_eq!(always_pass.structure, "minecraft:empty"); + } } diff --git a/crates/pumpkin/src/net/java/config/known_packs.rs b/crates/pumpkin/src/net/java/config/known_packs.rs index 7e45c3db9..06ca6f636 100644 --- a/crates/pumpkin/src/net/java/config/known_packs.rs +++ b/crates/pumpkin/src/net/java/config/known_packs.rs @@ -8,8 +8,10 @@ impl JavaClient { let version = self.version.load(); if version.supports_configuration_state() { - self.send_packet(&CFeatureFlags::new(&["minecraft:vanilla".to_string()])) - .await; + if version < JavaMinecraftVersion::V_1_20_5 { + let features = server.get_enabled_features(); + self.send_packet(&CFeatureFlags::new(&features)).await; + } let test_instance_entries = server.datapack_manager.get_test_instance_registry_entries(); diff --git a/crates/pumpkin/src/net/java/config/mod.rs b/crates/pumpkin/src/net/java/config/mod.rs index 6167e3ab5..0b2a386ab 100644 --- a/crates/pumpkin/src/net/java/config/mod.rs +++ b/crates/pumpkin/src/net/java/config/mod.rs @@ -11,7 +11,7 @@ use crate::{ use core::str; use pumpkin_data::registry::Registry; use pumpkin_protocol::{ - ConnectionState, KnownPack, + ConnectionState, java::{ client::config::{CFeatureFlags, CFinishConfig, CKnownPacks, CRegistryData, CUpdateTags}, server::config::{ @@ -20,7 +20,7 @@ use pumpkin_protocol::{ }, }, }; -use pumpkin_util::{Hand, text::TextComponent}; +use pumpkin_util::{Hand, text::TextComponent, version::JavaMinecraftVersion}; use tracing::{debug, trace, warn}; const BRAND_CHANNEL_PREFIX: &str = "minecraft:brand"; diff --git a/crates/pumpkin/src/net/java/config/resource_pack.rs b/crates/pumpkin/src/net/java/config/resource_pack.rs index da174d573..1f008506e 100644 --- a/crates/pumpkin/src/net/java/config/resource_pack.rs +++ b/crates/pumpkin/src/net/java/config/resource_pack.rs @@ -69,16 +69,15 @@ impl JavaClient { self.id ); } - self.send_known_packs().await; + self.send_known_packs(server).await; } - pub async fn send_known_packs(&self) { + pub async fn send_known_packs(&self, server: &Server) { + let features = server.get_enabled_features(); + self.send_packet(&CFeatureFlags::new(&features)).await; let version_str = self.version.load().to_string(); - self.send_packet(&CKnownPacks::new(&[KnownPack { - namespace: "minecraft", - id: "core", - version: &version_str, - }])) - .await; + let loaded_packs = server.datapack_manager.get_loaded_packs(); + let known_packs = server.get_known_packs(&version_str, &loaded_packs); + self.send_packet(&CKnownPacks::new(&known_packs)).await; } } diff --git a/crates/pumpkin/src/net/java/login/known_packs.rs b/crates/pumpkin/src/net/java/login/known_packs.rs index 09c57893a..2102fe4d9 100644 --- a/crates/pumpkin/src/net/java/login/known_packs.rs +++ b/crates/pumpkin/src/net/java/login/known_packs.rs @@ -2,11 +2,13 @@ use super::*; impl PendingConnection { - pub async fn handle_known_packs(&mut self) { + pub async fn handle_known_packs(&mut self, server: &Server) { let version = self.version.load(); if version.supports_configuration_state() { - self.send_packet_now(&CFeatureFlags::new(&["minecraft:vanilla".to_string()])) - .await; + if version < JavaMinecraftVersion::V_1_20_5 { + let features = server.get_enabled_features(); + self.send_packet_now(&CFeatureFlags::new(&features)).await; + } let registry = pumpkin_data::registry::Registry::get_synced(version); for reg in ®istry { self.send_packet_now(&CRegistryData::new(®.registry_id, ®.registry_entries)) diff --git a/crates/pumpkin/src/net/java/login/login_acknowledged.rs b/crates/pumpkin/src/net/java/login/login_acknowledged.rs index 725d42c47..a0712267c 100644 --- a/crates/pumpkin/src/net/java/login/login_acknowledged.rs +++ b/crates/pumpkin/src/net/java/login/login_acknowledged.rs @@ -97,21 +97,20 @@ impl PendingConnection { self.send_packet_now(&resource_pack).await; } else if self.version.load() >= JavaMinecraftVersion::V_1_20_5 { - self.send_known_packs().await; + self.send_known_packs(server).await; } else { - self.handle_known_packs().await; + self.handle_known_packs(server).await; } debug!("login acknowledged"); None } - pub async fn send_known_packs(&mut self) { + pub async fn send_known_packs(&mut self, server: &Server) { + let features = server.get_enabled_features(); + self.send_packet_now(&CFeatureFlags::new(&features)).await; let version_str = self.version.load().to_string(); - self.send_packet_now(&CKnownPacks::new(&[KnownPack { - namespace: "minecraft", - id: "core", - version: &version_str, - }])) - .await; + let loaded_packs = server.datapack_manager.get_loaded_packs(); + let known_packs = server.get_known_packs(&version_str, &loaded_packs); + self.send_packet_now(&CKnownPacks::new(&known_packs)).await; } } diff --git a/crates/pumpkin/src/net/java/login/mod.rs b/crates/pumpkin/src/net/java/login/mod.rs index 150c76c3c..1f7e70658 100644 --- a/crates/pumpkin/src/net/java/login/mod.rs +++ b/crates/pumpkin/src/net/java/login/mod.rs @@ -1,7 +1,7 @@ use arc_swap::ArcSwap; use pumpkin_data::translation; use pumpkin_protocol::{ - ConnectionState, KnownPack, Label, Link, LinkType, + ConnectionState, Label, Link, LinkType, java::client::{ config::{ CConfigAddResourcePack, CConfigServerLinks, CFeatureFlags, CFinishConfig, CKnownPacks, diff --git a/crates/pumpkin/src/net/java/pending.rs b/crates/pumpkin/src/net/java/pending.rs index b00d291cb..abf2eb94b 100644 --- a/crates/pumpkin/src/net/java/pending.rs +++ b/crates/pumpkin/src/net/java/pending.rs @@ -420,7 +420,7 @@ impl PendingConnection { } } id if id == SKnownPacks::to_id(version) => { - self.handle_known_packs().await; + self.handle_known_packs(server).await; Ok(None) } id if id == SConfigResourcePack::to_id(version) => { @@ -509,19 +509,33 @@ impl PendingConnection { match packet.response_result() { ResourcePackResponseResult::Downloaded | ResourcePackResponseResult::DownloadSuccess - | ResourcePackResponseResult::Accepted | ResourcePackResponseResult::Discarded - | ResourcePackResponseResult::Unknown(_) => {} + | ResourcePackResponseResult::Unknown(_) => { + if self.version.load() >= JavaMinecraftVersion::V_1_20_5 { + self.send_known_packs(server).await; + } else { + self.handle_known_packs(server).await; + } + } + ResourcePackResponseResult::Accepted => {} ResourcePackResponseResult::Declined => { if resource_config.force { self.kick(TextComponent::text("Required resource pack was declined")) .await; + } else if self.version.load() >= JavaMinecraftVersion::V_1_20_5 { + self.send_known_packs(server).await; + } else { + self.handle_known_packs(server).await; } } ResourcePackResponseResult::DownloadFail => { if resource_config.force { self.kick(TextComponent::text("Failed to download resource pack")) .await; + } else if self.version.load() >= JavaMinecraftVersion::V_1_20_5 { + self.send_known_packs(server).await; + } else { + self.handle_known_packs(server).await; } } ResourcePackResponseResult::InvalidUrl => { diff --git a/crates/pumpkin/src/net/java/play/client_command.rs b/crates/pumpkin/src/net/java/play/client_command.rs index 6c323785d..afdb65c15 100644 --- a/crates/pumpkin/src/net/java/play/client_command.rs +++ b/crates/pumpkin/src/net/java/play/client_command.rs @@ -67,9 +67,25 @@ impl JavaClient { return; } - let level_info = player.world().level_info.load(); + let world = player.world(); + let level_info = world.level_info.load(); + let minecart_improvements_enabled = world.server.upgrade().map_or_else( + || { + level_info + .data_packs + .enabled + .iter() + .any(|p| p == "minecart_improvements" || p == "file/minecart_improvements") + }, + |s| s.is_feature_enabled("minecraft:minecart_improvements"), + ); + let rules: Vec<(String, String)> = GameRule::all() .iter() + .filter(|rule| match rule { + GameRule::MaxMinecartSpeed => minecart_improvements_enabled, + _ => true, + }) .map(|rule| { ( rule.to_string(), diff --git a/crates/pumpkin/src/net/java/play/set_game_rule.rs b/crates/pumpkin/src/net/java/play/set_game_rule.rs index cde6304d5..41d37e5f5 100644 --- a/crates/pumpkin/src/net/java/play/set_game_rule.rs +++ b/crates/pumpkin/src/net/java/play/set_game_rule.rs @@ -13,6 +13,20 @@ impl JavaClient { return; } + let world = player.world(); + let minecart_improvements_enabled = world.server.upgrade().map_or_else( + || { + world + .level_info + .load() + .data_packs + .enabled + .iter() + .any(|p| p == "minecart_improvements" || p == "file/minecart_improvements") + }, + |s| s.is_feature_enabled("minecraft:minecart_improvements"), + ); + for entry in &packet.entries { let key = entry .game_rule_key @@ -23,6 +37,14 @@ impl JavaClient { continue; }; + if *rule == GameRule::MaxMinecartSpeed && !minecart_improvements_enabled { + warn!( + "Player {} tried to set game rule {} which is not enabled by feature flags", + player.gameprofile.name, entry.game_rule_key + ); + continue; + } + let level_info = player.world().level_info.load(); let current_val = level_info.game_rules.get(rule); match current_val { diff --git a/crates/pumpkin/src/server/mod.rs b/crates/pumpkin/src/server/mod.rs index 66532fdb0..bd2daf9fd 100644 --- a/crates/pumpkin/src/server/mod.rs +++ b/crates/pumpkin/src/server/mod.rs @@ -564,6 +564,26 @@ impl Server { } } + #[must_use] + pub fn get_known_packs<'a>( + &self, + server_version: &'a str, + loaded_packs: &'a [crate::data::datapack::LoadedDatapack], + ) -> Vec> { + self.datapack_manager + .get_known_packs(self, server_version, loaded_packs) + } + + #[must_use] + pub fn get_enabled_features(&self) -> Vec<&'static str> { + self.datapack_manager.get_enabled_features(self) + } + + #[must_use] + pub fn is_feature_enabled(&self, feature: &str) -> bool { + self.datapack_manager.is_feature_enabled(self, feature) + } + pub async fn save_all(&self) -> Result<(), String> { if let Err(err) = self.save_world_info() { error!("Failed to save world info: {err}"); diff --git a/crates/pumpkin/src/server/server_test_manager.rs b/crates/pumpkin/src/server/server_test_manager.rs index 36646e59a..315419ea4 100644 --- a/crates/pumpkin/src/server/server_test_manager.rs +++ b/crates/pumpkin/src/server/server_test_manager.rs @@ -7,6 +7,7 @@ use pumpkin_data::{BlockState, BlockStateId}; use pumpkin_gametest::{ BlockBasedTest, GameTestError, GameTestManager, GameTestReporter, GameTestResult, GameTestRotation, GameTestRunner, GameTestSession, GameTestStructureTemplate, GameTestWorld, + TestType, }; pub use pumpkin_gametest::{GameTestBatchReport, GameTestRetryOptions}; use pumpkin_nbt::NbtCompound; @@ -149,6 +150,13 @@ async fn prepare_test_run( GameTestError::World(format!("Unknown test instance '{}'", request.test_id)) })?; + if test_instance.instance_type != TestType::BlockBased { + return Err(GameTestError::World(format!( + "Test instance '{}' has unsupported type '{:?}' (only block-based tests can be executed currently)", + request.test_id, test_instance.instance_type + ))); + } + let structure = server .datapack_manager .load_structure(&test_instance.structure)