From f7190cff673aefc9d2d61c171e20f73533155750 Mon Sep 17 00:00:00 2001 From: Riccardo Guerriero Date: Fri, 6 Nov 2026 20:28:43 +0100 Subject: [PATCH] Widen four parity harnesses' inputs Cover all block states, registered enchantments, lore styles and representative Lunge tiers using the existing vanilla oracles. Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) Co-Authored-By: Codex (gpt-6.1-sol) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WZzq8KX3vDMZy53y1vXqeT --- tools/test_block_tint_parity.py | 18 +++- tools/test_enchantment_components_parity.py | 31 +++++++ tools/test_lore_tooltip_parity.py | 92 ++++++++++++++++++++- tools/test_lunge_motion_parity.py | 40 ++++++--- 4 files changed, 161 insertions(+), 20 deletions(-) diff --git a/tools/test_block_tint_parity.py b/tools/test_block_tint_parity.py index 89ef25738a..cfbc9be6a8 100644 --- a/tools/test_block_tint_parity.py +++ b/tools/test_block_tint_parity.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 """Compare level-free block-model tints with actual 26.3 BlockTintSources. -Every state of blocks with registered sources, plus untinted carried blocks, -is checked at indices -1, 0, 1 and 2. Geometry and framebuffer are separate checks. +Every state of every registered block -- not only the hand-picked tinted +families -- is checked at indices -1, 0, 1 and 2: a block Horizon tints and +vanilla does not (or the other way round) only shows up in that sweep. +Geometry and framebuffer are separate checks. """ import json try: @@ -18,9 +20,15 @@ attached_pumpkin_stem melon_stem pumpkin_stem lily_pad stone poppy red_mushroom brown_mushroom crimson_fungus warped_roots carved_pumpkin""".split() +def sweep(states): + """The hand-picked families plus every other registered block name.""" + names = {name.split(':', 1)[-1] for name in states.by_name} + return sorted(set(BLOCKS) | names) + + def make_input(build): states = mesh.States(lab.RESOURCES) - ids = sorted({state for name in BLOCKS for state in states.by_name["minecraft:" + name]}) + ids = sorted({state for name in sweep(states) for state in states.by_name.get("minecraft:" + name, [])}) rows = [[state, index] for state in ids for index in [-1, 0, 1, 2]] path = build / "input.json" path.write_text(json.dumps(dict(resources=str(lab.RESOURCES), cases=rows))) @@ -29,7 +37,9 @@ def make_input(build): def main(): harness.differential("block-tint-parity", "VanillaBlockTintOracle", "block_tint_probe", - make_input, "BlockTintSource.color(state), all states of tinted blocks and untinted carried blocks; per-face tint indices, no level/biome. Not model or framebuffer parity.", + make_input, "BlockTintSource.color(state) at indices -1, 0, 1 and 2 for every state of every registered block, " + "not just the hand-picked tinted families; per-face tint indices, no level/biome. " + "Not model or framebuffer parity.", sources=[lab.ROOT / "tools/test_block_tint_parity.py", lab.ROOT / "crates/horizon-client-render/src/mesher.rs", lab.ROOT / "crates/horizon-client/src/item_render.rs"]) diff --git a/tools/test_enchantment_components_parity.py b/tools/test_enchantment_components_parity.py index 4dce5b2688..8c8f364a96 100644 --- a/tools/test_enchantment_components_parity.py +++ b/tools/test_enchantment_components_parity.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Compare persistent enchantment levels and display names with vanilla 26.3.""" +import json import os from pathlib import Path import subprocess @@ -11,6 +12,16 @@ except ImportError: import test_rust_parity as base +def registry(): + """Every enchantment the prepared data pack registers, with its max_level.""" + root = lab.RESOURCES / 'data/minecraft/enchantment' + out = [] + for path in sorted(root.glob('*.json')): + data = json.loads(path.read_text(encoding='utf-8')) + out.append((f'minecraft:{path.stem}', int(data.get('max_level', 1)))) + return out + + def corpus(): values = [ '-2147483648', '-1', '0', '1', '10', '11', '254', '255', '256', '32767', @@ -26,6 +37,26 @@ def corpus(): for label, levels in cases: yield (f'{index}:{label}\t{{"minecraft:damage":12,"{key}":' f'{levels},"{other}":{{mending:1}}}}') + # Every registered enchantment at every interesting level, alone and beside a + # second enchantment: the level ranges each definition declares (its + # max_level, one below, one above, far above), the byte range the component + # stores, and levels the display name turns into a numeral or a number. + enchantments = registry() + for index, key in enumerate(keys): + other = keys[1 - index] + for name, maximum in enchantments: + levels = sorted({-2147483648, -1, 0, 1, 2, max(1, maximum - 1), maximum, + maximum + 1, 255, 256, 32767, 2147483647}) + for level in levels: + yield (f'{index}:{name[10:]}-{level}\t{{"minecraft:damage":12,"{key}":' + f'{{"{name}":{level}}},"{other}":{{"minecraft:mending":1}}}}') + for level in sorted({0, 1, maximum, maximum + 1}) if name != "minecraft:sharpness" else (): + yield (f'{index}:{name[10:]}-{level}-pair\t{{"minecraft:damage":12,"{key}":' + f'{{"minecraft:sharpness":{maximum},"{name}":{level}}}}}') + for label, level in [('all-max', None), ('all-one', 1), ('all-zero', 0)]: + levels = {name: (maximum if level is None else level) for name, maximum in enchantments} + body = ','.join(f'"{name}":{value}' for name, value in levels.items()) + yield f'{index}:{label}\t{{"minecraft:damage":12,"{key}":{{{body}}}}}' def main(): diff --git a/tools/test_lore_tooltip_parity.py b/tools/test_lore_tooltip_parity.py index eace073ec2..c4c3d0b88f 100644 --- a/tools/test_lore_tooltip_parity.py +++ b/tools/test_lore_tooltip_parity.py @@ -5,6 +5,8 @@ Vanilla prints what ``ItemLore.addToTooltip`` adds after ``ComponentUtils.mergeS with dark purple italic (VanillaLoreTooltipOracle). Horizon prints what ``item_tooltip::lore_texts`` gives (examples/probe/lore_tooltip_fixture). """ +import json + try: from . import lab, oracle_harness as harness except ImportError: @@ -30,21 +32,103 @@ CASES = [ ] +COLORS = ['black', 'dark_blue', 'dark_green', 'dark_aqua', 'dark_red', 'dark_purple', 'gold', + 'gray', 'dark_gray', 'blue', 'green', 'aqua', 'red', 'light_purple', 'yellow', 'white', + '#000000', '#ffffff', '#12ab34', '#c0ffee'] +STYLES = ['bold', 'italic', 'underlined', 'strikethrough', 'obfuscated'] + + +def stack(entries, display=None): + """SNBT of a stick whose lore is the given list of NBT text components.""" + components = '"minecraft:lore":[' + ','.join(entries) + ']' + if display is not None: + components += ',"minecraft:tooltip_display":' + display + return '{id:"minecraft:stick",count:1,components:{' + components + '}}' + + +def component(value): + """A lore entry: a plain string stays a string, an object is JSON in NBT single quotes.""" + return "'" + json.dumps(value, separators=(',', ':'), ensure_ascii=False) + "'" + + +def lore(*entries): + return stack([component(entry) for entry in entries]) + + +def generated(): + """Styles, colours and nesting across the whole text-component surface.""" + out = [] + for color in COLORS: + out.append(lore({'text': 'C', 'color': color})) + for style in STYLES: + out.append(lore({'text': 'S', style: True})) + out.append(lore({'text': 'S', style: False})) + # The dark purple italic line style under an explicit false, and a child + # that turns the line's style back on or off. + out.append(lore({'text': 'P', style: False, 'extra': [{'text': 'C', style: True}]})) + out.append(lore({'text': 'P', style: True, 'extra': [{'text': 'C', style: False}]})) + out.append(lore({'text': 'P', style: True, 'extra': ['plain']})) + for color in COLORS[:16]: + for style in STYLES: + out.append(lore({'text': 'P', 'color': color, style: True, + 'extra': [{'text': 'C', 'color': 'red', 'italic': False}]})) + # Nesting chains: each level overrides one more field. + for depth in range(1, 5): + child = {'text': 'd' + str(depth), 'color': COLORS[depth % len(COLORS)], + 'bold': bool(depth % 2), 'italic': bool((depth + 1) % 2), + 'underlined': bool(depth % 3 == 0), 'strikethrough': bool(depth % 2 == 0)} + for level in range(depth - 1, 0, -1): + child = {'text': 'd' + str(level), 'color': COLORS[level % len(COLORS)], + 'extra': [child], 'bold': bool(level % 2), + 'obfuscated': bool(level % 4 == 0)} + out.append(lore(child)) + # Siblings at one level, one plain string and one object, and empty texts. + out.append(lore({'extra': [{'text': 'A', 'color': 'gold'}, 'B', {'text': '', 'color': 'red'}, + {'extra': [{'text': 'C', 'italic': False}]}]})) + out.append(lore({'text': ''})) + out.append(lore({'text': '', 'color': 'aqua'})) + out.append(lore('', {'text': ''})) + # Many lines: order, mixed styles and repeated colours. + out.append(lore(*[{'text': 'L' + str(i), 'color': COLORS[i % len(COLORS)], + 'bold': bool(i % 2), 'italic': bool(i % 3 == 0)} for i in range(10)])) + out.append(lore(*['line' + str(i) for i in range(6)])) + # Text outside ASCII, with styles, and a nested non-ASCII child. + out.append(lore({'text': '\u00e9\u4e2d\U0001F600', 'color': 'gold', 'bold': True})) + out.append(lore({'text': 'p', 'extra': [{'text': '\U0001F600'}], 'italic': False})) + # tooltip_display: hidden lore, another hidden component, an empty and a + # repeated entry, and hide_tooltip (which must not hide the lore lines). + out.append(stack([component({'text': 'H'})], '{hidden_components:["minecraft:lore"]}')) + out.append(stack([component({'text': 'H'})], '{hidden_components:["minecraft:enchantments"]}')) + out.append(stack([component({'text': 'H'})], '{hidden_components:[]}')) + out.append(stack([component({'text': 'H'})], '{hidden_components:["minecraft:lore","minecraft:lore"]}')) + out.append(stack([component({'text': 'H'})], '{hide_tooltip:1b}')) + out.append(stack([component({'text': 'H'})], '{hide_tooltip:1b,hidden_components:["minecraft:enchantments"]}')) + out.append(stack([component({'text': 'H'})], '{hide_tooltip:0b,hidden_components:["minecraft:lore"]}')) + return out + + +def corpus(): + return CASES + generated() + + def cases(build): + lines = corpus() path = build / 'cases.txt' - path.write_text(''.join(line + '\n' for line in CASES)) - return path, len(CASES) + path.write_text(''.join(line + '\n' for line in lines)) + return path, len(lines) if __name__ == '__main__': harness.differential( 'lore-tooltip-parity', 'VanillaLoreTooltipOracle', 'lore_tooltip_fixture', cases, 'Item lore tooltip lines: dark purple italic, child colours, explicit non-italic, a line with no text of its own,' - ' plain-string children and packed colours, and tooltip_display', + ' plain-string children and packed colours, and tooltip_display; every named and packed colour, every style flag' + ' explicit and inherited, nesting four levels deep, sibling strings and objects, empty texts, ten-line lore,' + ' non-ASCII text and the hidden_components/hide_tooltip combinations', sources=[ lab.ROOT / 'crates/horizon-client/src/item_tooltip.rs', lab.ROOT / 'crates/horizon-items/src/item_data.rs', lab.ROOT / 'crates/horizon-client-core/src/text.rs', lab.ROOT / 'crates/horizon-client-core/src/component.rs', ], - expected_lines=len(CASES)) + expected_lines=len(corpus())) diff --git a/tools/test_lunge_motion_parity.py b/tools/test_lunge_motion_parity.py index 52344874e6..e6759fa88a 100644 --- a/tools/test_lunge_motion_parity.py +++ b/tools/test_lunge_motion_parity.py @@ -17,17 +17,33 @@ except ImportError: NAME = 'lunge-motion-parity' # server_dir and generate_world write these server.properties. -locator.PROPERTIES = dict(locator.PROPERTIES, gamemode='survival') -SCRIPT = [ - # The jump ends on the ground: the join's position said otherwise, and a server's copy goes by what it is told. - ('setup', ['cmd Alice give @s minecraft:netherite_spear[enchantments={lunge:3}]', 'arc Alice -1']), - ('ground', ['stab Alice']), - ('same-tick', ['arc Alice 0']), - ('rising', ['arc Alice 3']), - ('falling', ['arc Alice 8']), +locator.PROPERTIES = dict(locator.PROPERTIES, gamemode='survival', difficulty='peaceful') + +# Exercise the lowest and an intermediate tier/level as well as the original maximum. +# The extra tiers jab standing: repeating every jump phase drifts Alice far enough +# from the spawn that vanilla's own real-time steps occasionally miss the jab, and +# the original maximum keeps all four phases (the same distance as before). +PHASES = [('ground', ['stab Alice']), ('same-tick', ['arc Alice 0']), ('rising', ['arc Alice 3']), + ('falling', ['arc Alice 8'])] +GROUND = [PHASES[0]] +SPEARS = [('wooden_spear', 1, GROUND), ('iron_spear', 2, GROUND), ('netherite_spear', 3, PHASES)] + + +def script(): + # Peaceful: the flat world's slimes would otherwise hit Alice while the + # script is idle before the first command (and kill her), adding motions. + steps = [('setup', ['arc Alice -1'])] + for spear, level, phases in SPEARS: + steps.append(('setup', ['cmd Alice clear @s', + f'cmd Alice give @s minecraft:{spear}[enchantments={{lunge:{level}}}]'])) + for phase, actions in phases: + steps.append((f'{spear}-{level}-{phase}', list(actions))) # jumpFromGround gives a sprinting server copy 0.2 forward too. - ('sprinting', ['sprint Alice', 'arc Alice 3']), -] + steps.append(('sprinting', ['sprint Alice', 'arc Alice 3'])) + return steps + + +SCRIPT = script() def shown(lines): @@ -38,13 +54,13 @@ def shown(lines): def check(vanilla, vanilla_all): steps = {line.split()[1] for line in vanilla} - missing = {f'step={step}' for step, _ in SCRIPT[1:]} - steps + missing = {f'step={step}' for step, _ in SCRIPT if step != 'setup'} - steps return f'vanilla sent no motion in {sorted(missing)}' if missing else None def main(): lines = locator.one_player(NAME, SCRIPT, shown, check, "A spear's Lunge standing and at three points of a jump " - 'on a dedicated server: the motion the player is sent.', step_seconds=2.0) + 'on a dedicated server: motion for wooden Lunge I, iron Lunge II and netherite Lunge III.', step_seconds=2.0) print(f'lunge motion: {len(lines)} lines identical')