Merge pull request #11706 from Bioneisme/fix/damage-type-command-ids

🤖 Agent: /damage ids now come from the world's damage catalog and are replaced on each world open, so a pack's type doesn't leak into the next world (union of 74 heads on main: fmt, clippy -D warnings, tests green).
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-07 12:30:08 +01:00
commit ee8a79e80e
2 files changed
+148

No files matched your search

@@ -275,6 +275,25 @@ 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. 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) {
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] {
if let Some(r) = self.by_registry.get(registry) {
@@ -312,3 +331,49 @@ 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"]);
}
// 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"]
);
}
}
+83
View File
@@ -684,6 +684,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).
@@ -2308,3 +2310,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"
);
}
}