From 6cc37bdcefbd0663fb82ce5424b4cc290d5143e9 Mon Sep 17 00:00:00 2001 From: bioneisme Date: Sat, 7 Nov 2026 14:24:35 +0500 Subject: [PATCH 1/2] /damage takes content mods' and world packs' damage types The commands' minecraft:damage_type ids came from the vanilla export alone, so /damage refused a type from a content mod or a world's data pack with argument.resource.not_found. Vanilla's ResourceArgument looks the id up in the server's registries, packs included. build_registries now adds the damage catalog's types to the registry's ids after the export is applied, keeping the export's tags. Co-Authored-By: Claude Opus 5.5 --- crates/horizon-client-core/src/registries.rs | 54 +++++++++++++ crates/horizon-client/src/game/chat.rs | 83 ++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/crates/horizon-client-core/src/registries.rs b/crates/horizon-client-core/src/registries.rs index 302051bf01..93155f6833 100644 --- a/crates/horizon-client-core/src/registries.rs +++ b/crates/horizon-client-core/src/registries.rs @@ -275,6 +275,21 @@ impl Registries { .clone_from(&e.advancement_criteria); } } + /// Add the damage catalog's types ([`Self::damage_types`]: content mods' + /// and the world's packs' with vanilla's) to the commands' + /// `minecraft:damage_type` ids, which the export lists for vanilla alone. + /// `ResourceArgument` looks a type up in the server's registries, so + /// `/damage` takes every type the world has. The export's tags stay. + pub fn sync_damage_type_ids(&mut self) { + let ids = &mut self + .by_registry + .entry("minecraft:damage_type".into()) + .or_default() + .ids; + ids.extend(self.damage_types.iter().cloned()); + ids.sort(); + ids.dedup(); + } /// The element ids of a registry (sorted). pub fn ids(&self, registry: &str) -> &[String] { if let Some(r) = self.by_registry.get(registry) { @@ -312,3 +327,42 @@ impl Registries { } } } + +#[cfg(test)] +mod tests { + use super::Registries; + + /// A content mod's damage type joins vanilla's in the commands' ids, the + /// export's tags stay, and a second world open lists each type once. + #[test] + fn damage_type_ids_take_the_catalogs_types() { + let export = Registries::from_export(&serde_json::json!({ + "by_registry": {"minecraft:damage_type": { + "ids": ["minecraft:fall", "minecraft:magic"], + "tags": ["minecraft:is_fall"], + }}, + })); + let mut reg = Registries { + damage_types: vec![ + "minecraft:magic".into(), + "example:test".into(), + "minecraft:fall".into(), + ], + ..Registries::default() + }; + reg.fill_from_exported(&export); + assert_eq!( + reg.ids("minecraft:damage_type"), + ["minecraft:fall", "minecraft:magic"], + "the export alone lists vanilla's types" + ); + for _ in 0..2 { + reg.sync_damage_type_ids(); + assert_eq!( + reg.ids("minecraft:damage_type"), + ["example:test", "minecraft:fall", "minecraft:magic"] + ); + assert_eq!(reg.tags("minecraft:damage_type"), ["minecraft:is_fall"]); + } + } +} diff --git a/crates/horizon-client/src/game/chat.rs b/crates/horizon-client/src/game/chat.rs index 522bcc0431..3644d8e495 100644 --- a/crates/horizon-client/src/game/chat.rs +++ b/crates/horizon-client/src/game/chat.rs @@ -685,6 +685,8 @@ impl Game<'_> { .map(|(k, _)| k.clone()) .collect(); self.sync_enchantment_registries(); + // After the export, which keys the registry with vanilla's tags. + self.ui.registries.sync_damage_type_ids(); } /// The block the player is looking at, for coordinate suggestions, and /// the entity, for entity arguments (ClientSuggestionProvider). @@ -2270,3 +2272,84 @@ mod enchantment_snapshot_tests { ); } } + +#[cfg(test)] +mod damage_type_registry_tests { + use crate::game::{Audio, Game, Options, Progress, Settings, load_resources}; + + /// A world pack's damage type is a `/damage` type once the world opens, + /// beside vanilla's, and the registry keeps vanilla's tags. + #[test] + #[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"] + fn a_world_packs_damage_type_parses_in_damage() { + let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap()); + let settings = Settings::default(); + let loaded = load_resources( + root.clone(), + None, + &root, + &settings, + std::time::Instant::now(), + false, + true, + &Progress::new(), + &mut |_, _| unreachable!(), + ) + .unwrap(); + let mut options = Options::parse(std::iter::empty()).unwrap().unwrap(); + options.resources = root.clone(); + options.execution_role = crate::game::execution_role::ExecutionRole::IntegratedServer; + let mut game = Game::new( + &loaded.resources, + loaded.pack, + loaded.gui.unwrap(), + options, + settings, + Audio::silent(&root), + None, + ) + .unwrap(); + + let dir = std::path::PathBuf::from(std::env::var_os("HORIZON_HOME").unwrap()) + .join("damage-type-ids"); + let pack = dir.join("datapacks/model"); + let types = pack.join("data/example/damage_type"); + std::fs::create_dir_all(&types).unwrap(); + std::fs::write(pack.join("pack.mcmeta"), "{}").unwrap(); + std::fs::write( + types.join("test.json"), + r#"{"message_id":"example.test","scaling":"never","exhaustion":0.0}"#, + ) + .unwrap(); + std::fs::write( + dir.join("datapacks.json"), + r#"{"enabled":["file/model"],"disabled":[]}"#, + ) + .unwrap(); + game.world_root = dir; + (game.world_tables, game.health_resources) = game.build_world_tables(false, &[]).unwrap(); + game.level.allow_commands = Some(true); + game.build_registries(); + + let reg = &game.ui.registries; + assert!( + reg.damage_types.contains(&"example:test".to_owned()), + "the damage catalog holds the pack's type" + ); + let parse = |input: &str| { + horizon_client::command::vanilla::dispatcher() + .parse(input, reg) + .executable() + .map(|_| ()) + .map_err(|e| format!("{e:?}")) + }; + assert_eq!(parse("damage @s 1 example:test"), Ok(())); + assert_eq!(parse("damage @s 1 minecraft:magic"), Ok(())); + assert!(parse("damage @s 1 example:missing").is_err()); + assert!( + reg.tags("minecraft:damage_type") + .contains(&"minecraft:is_fire".to_owned()), + "vanilla's damage type tags stay" + ); + } +} From ee026a522e9fbe5028c9894b5377685925e44bd6 Mon Sep 17 00:00:00 2001 From: Horizon agent Date: Sat, 7 Nov 2026 10:59:32 +0100 Subject: [PATCH 2/2] Replace the damage type ids instead of extending them by_registry outlives a world, so a pack's type stayed accepted by /damage in the next world opened. Same as sync_enchantment_registries. Co-Authored-By: Claude Opus 5.5 --- crates/horizon-client-core/src/registries.rs | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/horizon-client-core/src/registries.rs b/crates/horizon-client-core/src/registries.rs index 93155f6833..5f35ce8344 100644 --- a/crates/horizon-client-core/src/registries.rs +++ b/crates/horizon-client-core/src/registries.rs @@ -279,16 +279,20 @@ impl Registries { /// and the world's packs' with vanilla's) to the commands' /// `minecraft:damage_type` ids, which the export lists for vanilla alone. /// `ResourceArgument` looks a type up in the server's registries, so - /// `/damage` takes every type the world has. The export's tags stay. + /// `/damage` takes every type the world has. The export's tags stay. The + /// ids are replaced, not extended: `by_registry` outlives a world, and a + /// pack's type must not stay accepted in the next world opened. pub fn sync_damage_type_ids(&mut self) { - let ids = &mut self - .by_registry - .entry("minecraft:damage_type".into()) - .or_default() - .ids; - ids.extend(self.damage_types.iter().cloned()); + if self.damage_types.is_empty() { + return; + } + let mut ids = self.damage_types.clone(); ids.sort(); ids.dedup(); + self.by_registry + .entry("minecraft:damage_type".into()) + .or_default() + .ids = ids; } /// The element ids of a registry (sorted). pub fn ids(&self, registry: &str) -> &[String] { @@ -364,5 +368,12 @@ mod tests { ); assert_eq!(reg.tags("minecraft:damage_type"), ["minecraft:is_fall"]); } + // The next world, without the pack: its type is no longer accepted. + reg.damage_types = vec!["minecraft:fall".into(), "minecraft:magic".into()]; + reg.sync_damage_type_ids(); + assert_eq!( + reg.ids("minecraft:damage_type"), + ["minecraft:fall", "minecraft:magic"] + ); } }