Canonicalize bare map decoration component type IDs

Use the existing Identifier parser at preserved-component decode/encode boundaries. Own current Game/fresh Java focused gate94/103->103/103; unchanged full106 improves94/106->103/106 with exactly3 separate empty-elision reads retained. Core round-trip old-fail/fixed-pass,66item tests pass. Clippy, docs and content-root checks pass; existing Python release-fixture failures retained.

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
Florian ERNSTandCodex committed 2026-11-05 09:33:08 +01:00
1 parent 41d5649ed5
commit ebc1d17ea3
4 files changed
+163 -2

No files matched your search

+57 -2
View File
@@ -689,6 +689,27 @@ pub fn parse_stack(snbt: &str) -> Result<Option<ItemStack>, String> {
let (tag, _) = horizon_world::nbt::parse_snbt(snbt)?;
Ok(crate::item_nbt::item_from_nbt(&tag))
}
/// `MapDecorations.Entry.CODEC`: a bare type ID uses the default resource namespace.
/// Keep preserved entry keys and numeric tags intact; this is not registry validation.
fn canonical_other_component(id: &str, tag: &Tag) -> Tag {
let mut value = tag.clone();
if id == "minecraft:map_decorations"
&& let Tag::Compound(entries) = &mut value
{
for entry in entries.values_mut() {
if let Some(Tag::String(kind)) = entry.get_mut("type") {
let text = kind.to_string_lossy();
if !text.is_empty()
&& !text.contains(':')
&& let Ok(id) = horizon_world::identifier::Identifier::parse(&text)
{
*kind = id.to_string().as_str().into();
}
}
}
}
value
}
/// The `components` compound for a stack, or None when every component is default.
pub fn encode_components(stack: &ItemStack) -> Option<Tag> {
components_tag(stack.damage, stack.data())
@@ -897,7 +918,7 @@ pub fn components_tag(damage: u32, data: Option<&StackData>) -> Option<Tag> {
if let Some(d) = data {
for (k, v) in &d.other {
out.entry(JavaString::from(k.as_str()))
.or_insert_with(|| v.clone());
.or_insert_with(|| canonical_other_component(k, v));
}
}
(!out.is_empty()).then_some(Tag::Compound(out))
@@ -1068,7 +1089,8 @@ pub fn decode_components(components: Option<&Tag>) -> (u32, Option<Box<StackData
&& (matches!(key.as_str(), "minecraft:item_name" | "minecraft:rarity")
|| (key == "minecraft:tooltip_display" && v == &ominous_tooltip_display()));
if !MODELLED.contains(&key.as_str()) && !ominous_part {
data.other.push((key, v.clone()));
let value = canonical_other_component(&key, v);
data.other.push((key, value));
}
}
data.other.sort_by(|a, b| a.0.cmp(&b.0));
@@ -1108,6 +1130,39 @@ pub fn command_components(text: &str) -> Result<Tag, String> {
mod tests {
use super::*;
#[test]
fn map_decoration_types_round_trip_with_default_namespaces() {
let parse = |text| horizon_world::nbt::parse_snbt(text).unwrap().0;
let source = parse(
r#"{"minecraft:map_decorations":{a:{type:"minecraft:mansion",x:10.5d,z:-20.0d,rotation:90.0f},"a+b":{type:"red_x",x:-1.25d,z:2.75d,rotation:-45.5f}},"minecraft:custom_data":{keep:7}}"#,
);
let expected = parse(
r#"{"minecraft:map_decorations":{a:{type:"minecraft:mansion",x:10.5d,z:-20.0d,rotation:90.0f},"a+b":{type:"minecraft:red_x",x:-1.25d,z:2.75d,rotation:-45.5f}},"minecraft:custom_data":{keep:7}}"#,
);
assert!(decode_components(None).1.is_none());
for input in [&source, &expected] {
let (damage, extra) = decode_components(Some(input));
assert_eq!(
extra.as_ref().unwrap().other("minecraft:map_decorations"),
expected.get("minecraft:map_decorations")
);
let loaded = ItemStack {
damage,
extra,
..ItemStack::new("minecraft:filled_map", 1)
};
assert_eq!(encode_components(&loaded), Some(expected.clone()));
// A fresh model has no original NBT or retained component source.
let mut fresh = ItemStack::new("minecraft:filled_map", 1);
assert!(fresh.extra.is_none());
for id in ["minecraft:map_decorations", "minecraft:custom_data"] {
fresh.data_mut().set_other(id, input.get(id).cloned());
}
assert_eq!(encode_components(&fresh), Some(expected.clone()));
assert!(fresh.data().unwrap().map_id.is_none());
}
}
#[test]
fn components_round_trip() {
let mut stack = ItemStack::new("minecraft:diamond_sword", 1);
+67
View File
@@ -0,0 +1,67 @@
"""Map decoration resource IDs through the live item component codec, without MapData."""
from command_exec_corpus import cmd
def map_decoration_namespaces(empty_diagnostic=False):
ops = []
def run(command, expected=None, label=None, record=True):
op = cmd(command, expect_result=expected, record=record)
if label:
op['label'] = label
ops.append(op)
run('/tick freeze', 1)
for command in ['/gamerule spawn_mobs false', '/gamerule advance_time false',
'/gamemode creative', '/forceload add 0 0 15 15']:
run(command)
ops.append({'op': 'settle', 'required_chunks': [[0, 0]]})
run('/execute if loaded 0 200 0', 1)
run('/fill 0 199 0 15 199 15 stone', 256)
run('/fill 0 200 0 15 204 15 air')
run('/tp @s 0.5 200 0.5 0 0')
run('/scoreboard objectives add deco_clock dummy', 1)
run('/data get entity @s UUID', 4)
run('/data get entity @s Pos')
run('/data get entity @s Health', 20)
run('/data get entity @s SelectedItemSlot', 0)
run('/execute if entity @s[type=player,gamemode=creative,nbt={Health:20f,Pos:[0.5d,200d,0.5d]}]', 1)
run('/data modify storage h318:identity uuid set from entity @s UUID', record=False)
run('/execute store result score start deco_clock run time query gametime', record=False)
cases = [
('two_entries', 'filled_map',
'map_decorations={a:{type:"minecraft:mansion",x:10.5d,z:-20.0d,rotation:90.0f},b:{type:"red_x",x:1.0d,z:2.0d,rotation:0.0f}}'),
('bare', 'filled_map',
'map_decorations={bare:{type:"mansion",x:-1.25d,z:2.75d,rotation:-45.5f}}'),
('qualified', 'filled_map',
'map_decorations={qualified:{type:"minecraft:red_x",x:-0.5d,z:-2.25d,rotation:17.25f}}'),
('plus_key', 'filled_map',
'map_decorations={"a+b":{type:"red_x",x:-10.5d,z:20.25d,rotation:180.5f}},custom_data={audit:"keep",counter:7}'),
('empty', 'filled_map', 'map_decorations={}'),
('plain', 'filled_map', ''),
('ordinary', 'stone', 'custom_data={audit:"plain",counter:3}'),
]
for name, item, components in cases:
run('/clear @s')
run('/execute unless data entity @s Inventory[]', 1)
suffix = '[' + components + ']' if components else ''
run(f'/item replace entity @s hotbar.0 with minecraft:{item}{suffix}', 1, name+'-replace')
run(f'/execute if entity @s[nbt={{Inventory:[{{Slot:0b,id:"minecraft:{item}",count:1}}]}}]', 1)
run('/data get entity @s Inventory', label=name+'-inventory')
run('/data get entity @s SelectedItem', label=name+'-selected')
run('/data get entity @s Inventory[{Slot:0b}].components."minecraft:map_decorations"', label=name+'-decorations')
run('/execute unless data entity @s Inventory[{Slot:0b}].components."minecraft:map_id"', 1)
run('/execute if entity @s[type=player,gamemode=creative,nbt={Health:20f,Pos:[0.5d,200d,0.5d]}]', 1)
run('/execute if loaded 0 200 0', 1)
run('/execute if block 0 199 0 stone', 1)
run('/execute store success score changed deco_clock run data modify storage h318:identity uuid set from entity @s UUID', record=False)
run('/scoreboard players get changed deco_clock', 0)
run('/execute store result score elapsed deco_clock run time query gametime', record=False)
run('/scoreboard players operation elapsed deco_clock -= start deco_clock', record=False)
run('/scoreboard players get elapsed deco_clock', 0)
if not empty_diagnostic:
# Separate default-elision lead: retain its write ACK and every guard.
excluded = {'empty-inventory', 'empty-selected', 'empty-decorations'}
assert sum(op.get('label') in excluded for op in ops) == 3
ops = [op for op in ops if op.get('label') not in excluded]
return ops
+8
View File
@@ -128,6 +128,8 @@ def main():
parser.add_argument("--pufferfish-mob-fixture", action="store_true")
parser.add_argument("--pufferfish-mob-duration-diagnostic", action="store_true")
parser.add_argument("--pufferfish-mob-overlap-control", action="store_true")
parser.add_argument("--map-decoration-namespace-fixture", action="store_true")
parser.add_argument("--map-decoration-empty-diagnostic", action="store_true")
args = parser.parse_args()
build = args.work_dir.resolve() if args.work_dir else lab.LOCAL / 'command-exec-parity'
build.mkdir(parents=True, exist_ok=True)
@@ -192,6 +194,12 @@ def main():
if args.sulfur_power_load_diagnostic:
groups = [('setup', corpus_module.group_setup()),
('sulfur powered-load diagnostic', corpus_module.sulfur_cube_fuse_ticks(True))]
if args.map_decoration_namespace_fixture:
try:
from .map_decoration_namespace_corpus import map_decoration_namespaces
except ImportError:
from map_decoration_namespace_corpus import map_decoration_namespaces
groups = [("setup", corpus_module.group_setup()), ("map decoration namespaces", map_decoration_namespaces(args.map_decoration_empty_diagnostic))]
if args.mob_quad_fixture:
try:
from .mob_quad_leash_corpus import mob_quad_leash
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Exact namespace round trips at zero world ticks: focused103 or full106 empty diagnostic."""
import argparse
from pathlib import Path
import subprocess
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', type=Path)
parser.add_argument('--reference-only', action='store_true')
parser.add_argument('--prepare', action='store_true')
parser.add_argument("--empty-diagnostic", action="store_true", help="unchanged full106, including the separate empty-component elision residual")
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
command = [sys.executable, str(root / 'tools/test_command_exec_parity.py'),
'--map-decoration-namespace-fixture', '--work-dir',
str(root / '.local/map-decoration-namespace-parity')]
if args.empty_diagnostic:
command.append("--map-decoration-empty-diagnostic")
if args.binary:
command += ['--binary', str(args.binary.resolve())]
for name in ['reference_only', 'prepare']:
if getattr(args, name):
command.append('--' + name.replace('_', '-'))
return subprocess.run(command, cwd=root).returncode
if __name__ == '__main__':
sys.exit(main())