Files
horizon-airdef/tools/test_item_wire_parity.py
T
some-du6e 9223b401e7 Decode every item data component a server sends
Horizon decoded 25 of 26.3's 122 data component types. Item data has no
length prefix, so a stack with any other component (an armour trim,
banner patterns, pot sherds, a stew's effects, a plugin's item) made the
whole packet undecodable and it was dropped: on a Paper server the
inventory arrived empty until each slot was clicked, the advancements
screen said "There doesn't seem to be anything here..." and the recipe
book was empty.

Every other component is now read to its end with its vanilla
StreamCodec (item/skip.rs) and kept as its bytes, written back as they
came; components without a network codec go as their NBT. The client
leaves them out of the item's components, so the item keeps its
prototype's values for them.

tools/test_item_wire_parity.py checks it against vanilla: the oracle
encodes 374 stacks with ItemStack.OPTIONAL_STREAM_CODEC (every default
component value of every item, SNBT fixtures for the rest, direct
holders and removals built in Java), covering all 122 component types,
and the Rust probe must consume every byte, find the same component ids
and encode the read-past ones back to the same bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 12:20:45 -06:00

178 lines
14 KiB
Python
Executable File

#!/usr/bin/env python3
"""Item stacks on the wire: every data component, as vanilla 26.3 encodes it.
VanillaItemWireOracle encodes stacks with ItemStack.OPTIONAL_STREAM_CODEC
(what a server sends in inventories, recipe books and advancements): each
default component value of every vanilla item, the SNBT fixtures below
(direct holders, tags and lists, optional fields, every consume effect,
block predicates with component matchers, the mob variants) and patches
built in Java (components without a persistent codec, removals). The Rust
probe decodes each with horizon_protocol::item::Slot and must consume every
byte and find the same component ids in the same order; a stack whose
components Horizon only reads past must also encode back to the same bytes.
Every id in DataComponents must be covered."""
import argparse
import json
import os
import platform
import subprocess
from pathlib import Path
try:
from . import lab, test_rust_parity as base
except ImportError:
import lab
import test_rust_parity as base
COMPONENT_TYPES = 122
FIXTURES = [
('custom_data', '{custom_data:{a:1,b:"x",c:[1,2]}}'),
('unbreakable', '{unbreakable:{}}'),
('custom_name', '{custom_name:{text:"Hi",color:"red",bold:1b}}'),
('lore', '{lore:["a",{text:"b",italic:0b}]}'),
('enchantments', '{enchantments:{"minecraft:sharpness":5,"minecraft:unbreaking":3}}'),
('can_break tag', '{can_break:{blocks:"#minecraft:logs"}}'),
('can_break list state nbt', '{can_break:[{blocks:["minecraft:stone","minecraft:dirt"],state:{snowy:"true",age:{min:"1",max:"3"},level:{min:"2"}},nbt:"{a:1b}"}]}'),
('can_place_on components', '{can_place_on:{blocks:"minecraft:chest",components:{"minecraft:custom_name":"x","minecraft:damage":3},predicates:{"minecraft:custom_data":{a:1},"minecraft:damage":{durability:{min:1}}}}}'),
('can_place_on empty', '{can_place_on:[{}]}'),
('custom_model_data', '{custom_model_data:{floats:[1.0f,2.5f],flags:[1b,0b],strings:["a"],colors:[16711680]}}'),
('tooltip_display', '{tooltip_display:{hide_tooltip:1b,hidden_components:["minecraft:enchantments","minecraft:damage"]}}'),
('repair_cost', '{repair_cost:7}'),
('intangible_projectile', '{intangible_projectile:{}}'),
('tooltip_style', '{tooltip_style:"minecraft:fancy"}'),
('interact_animation', '{interact_animation:{type:"stab",duration:9}}'),
('dyed_color', '{dyed_color:16711935}'),
('map_id', '{map_id:42}'),
('written_book_content', '{written_book_content:{title:{raw:"T",filtered:"F"},author:"me",generation:1,pages:[{raw:"p1",filtered:"q1"},"p2"],resolved:1b}}'),
('trim registry', '{trim:{material:"minecraft:gold",pattern:"minecraft:coast"}}'),
('trim direct', '{trim:{material:{palette_id:"minecraft:x",description:"X"},pattern:{asset_id:"minecraft:y",description:"Y",decal:1b}}}'),
('block_entity_data', '{block_entity_data:{id:"minecraft:chest",Items:[]}}'),
('lodestone_tracker target', '{lodestone_tracker:{target:{dimension:"minecraft:the_nether",pos:[1,-64,3]},tracked:0b}}'),
('lodestone_tracker empty', '{lodestone_tracker:{}}'),
('firework_explosion', '{firework_explosion:{shape:"star",colors:[1,2],fade_colors:[3],has_trail:1b,has_twinkle:0b}}'),
('profile name', '{profile:{name:"Notch"}}'),
('profile full', '{profile:{name:"jeb_",id:[I;1,2,3,4],properties:[{name:"textures",value:"abc",signature:"sig"}]}}'),
('note_block_sound', '{note_block_sound:"minecraft:block.note_block.bell"}'),
('base_color', '{base_color:"lime"}'),
('sulfur_cube_content', '{sulfur_cube_content:{id:"minecraft:stone",count:1}}'),
('lock', '{lock:{items:"minecraft:diamond"}}'),
('container_loot', '{container_loot:{loot_table:"minecraft:chests/simple_dungeon",seed:5L}}'),
('waxed', '{waxed:{}}'),
('banner_patterns', '{banner_patterns:[{pattern:"minecraft:stripe_bottom",color:"red"},{pattern:{asset_id:"minecraft:x",translation_key:"y"},color:"blue"}]}'),
('pot_decorations', '{pot_decorations:{back:{id:"minecraft:brick"},left:{id:"minecraft:angler_pottery_sherd"},front:{id:"minecraft:skull_pottery_sherd",count:1}}}'),
('suspicious_stew_effects', '{suspicious_stew_effects:[{id:"minecraft:night_vision",duration:100},{id:"minecraft:poison"}]}'),
('potion_contents full', '{potion_contents:{potion:"minecraft:swiftness",custom_color:123,custom_effects:[{id:"minecraft:speed",amplifier:1b,duration:200,ambient:1b,show_particles:0b,show_icon:1b,hidden_effect:{id:"minecraft:speed",duration:100,hidden_effect:{id:"minecraft:speed",duration:50}}}],custom_name:"x"}}'),
('potion_contents bare', '{potion_contents:{}}'),
('container', '{container:[{slot:0,item:{id:"minecraft:stone",count:3}},{slot:5,item:{id:"minecraft:diamond_sword",components:{"minecraft:damage":5,"minecraft:trim":{material:"minecraft:gold",pattern:"minecraft:coast"}}}}]}'),
('bundle_contents', '{bundle_contents:[{id:"minecraft:stone",count:2},{id:"minecraft:potion",components:{"minecraft:potion_contents":{potion:"minecraft:water"}}}]}'),
('charged_projectiles', '{charged_projectiles:[{id:"minecraft:arrow"},{id:"minecraft:firework_rocket",components:{"minecraft:fireworks":{flight_duration:2b,explosions:[{shape:"burst",colors:[5]}]}}}]}'),
('block_state', '{block_state:{facing:"north",half:"top"}}'),
('bees', '{bees:[{entity_data:{id:"minecraft:bee",Health:5f},ticks_in_hive:5,min_ticks_in_hive:10}]}'),
('entity_data', '{entity_data:{id:"minecraft:pig",NoAI:1b}}'),
('bucket_entity_data', '{bucket_entity_data:{Health:5f}}'),
('instrument direct', '{instrument:{sound_event:{sound_id:"minecraft:x",range:8f},use_duration:3f,range:64f,description:"Horn"}}'),
('painting variant', '{"minecraft:painting/variant":"minecraft:kebab"}'),
('sign_text_front', '{sign_text_front:{messages:["a","b","c","d"],filtered_messages:["e","f","g","h"],color:"red",has_glowing_text:1b}}'),
('sign_text_back', '{sign_text_back:{messages:["a","","",""]}}'),
('map_decorations', '{map_decorations:{a:{type:"minecraft:player",x:1.0d,z:2.0d,rotation:0f}}}'),
('debug_stick_state', '{debug_stick_state:{"minecraft:oak_stairs":"facing"}}'),
('recipes', '{recipes:["minecraft:stone_bricks"]}'),
('attribute_modifiers', '{attribute_modifiers:[{type:"minecraft:attack_damage",id:"minecraft:x",amount:1.5d,operation:"add_value",slot:"mainhand",display:{type:"override",value:"hi"}},{type:"minecraft:armor",id:"minecraft:y",amount:2d,operation:"add_multiplied_total",display:{type:"hidden"}}]}'),
('consumable all effects', '{consumable:{consume_seconds:2f,animation:"drink",sound:{sound_id:"minecraft:x",range:4f},has_consume_particles:0b,on_consume_effects:[{type:"apply_effects",effects:[{id:"minecraft:speed",duration:20}],probability:0.5f},{type:"remove_effects",effects:"minecraft:poison"},{type:"remove_effects",effects:["minecraft:poison","minecraft:wither"]},{type:"clear_all_effects"},{type:"teleport_randomly",diameter:8f},{type:"play_sound",sound:"minecraft:entity.generic.eat"}]}}'),
('death_protection', '{death_protection:{death_effects:[{type:"clear_all_effects"}]}}'),
('equippable full', '{equippable:{slot:"head",equip_sound:"minecraft:item.armor.equip_gold",asset_id:"minecraft:gold",camera_overlay:"minecraft:misc/pumpkinblur",allowed_entities:"#minecraft:skeletons",dispensable:0b,swappable:0b,damage_on_hurt:0b,equip_on_interact:1b,can_be_sheared:1b,shearing_sound:{sound_id:"minecraft:y"}}}'),
('equippable bare', '{equippable:{slot:"chest"}}'),
('tool rules', '{tool:{rules:[{blocks:"#minecraft:mineable/pickaxe",speed:4f,correct_for_drops:1b},{blocks:["minecraft:stone"]}],default_mining_speed:2f,damage_per_block:2,can_destroy_blocks_in_creative:0b}}'),
('blocks_attacks full', '{blocks_attacks:{block_delay_seconds:0.5f,disable_cooldown_scale:2f,damage_reductions:[{horizontal_blocking_angle:45f,type:"#minecraft:is_fire",base:1f,factor:0.5f},{base:0f,factor:1f}],item_damage:{threshold:3f,base:1f,factor:2f},bypassed_by:"#minecraft:bypasses_shield",block_sound:"minecraft:item.shield.block",disable_sound:{sound_id:"minecraft:z"}}}'),
('damage_resistant', '{damage_resistant:{types:"#minecraft:is_fire"}}'),
('repairable list', '{repairable:{items:["minecraft:diamond","minecraft:emerald"]}}'),
('use_cooldown group', '{use_cooldown:{seconds:1.5f,cooldown_group:"minecraft:x"}}'),
('use_remainder', '{use_remainder:{id:"minecraft:bowl",count:2}}'),
('fireworks', '{fireworks:{flight_duration:3b,explosions:[{shape:"creeper",colors:[1],fade_colors:[2,3],has_trail:1b},{shape:"small_ball"}]}}'),
('provides_banner_patterns', '{provides_banner_patterns:"#minecraft:pattern_item/globe"}'),
('wolf variant', '{"minecraft:wolf/variant":"minecraft:ashen","minecraft:wolf/sound_variant":"minecraft:angry","minecraft:wolf/collar":"red"}'),
('fox salmon parrot', '{"minecraft:fox/variant":"snow","minecraft:salmon/size":"large","minecraft:parrot/variant":"blue"}'),
('tropical fish', '{"minecraft:tropical_fish/pattern":"flopper","minecraft:tropical_fish/base_color":"white","minecraft:tropical_fish/pattern_color":"black"}'),
('mooshroom rabbit', '{"minecraft:mooshroom/variant":"brown","minecraft:rabbit/variant":"gold"}'),
('pig cow chicken', '{"minecraft:pig/variant":"minecraft:cold","minecraft:pig/sound_variant":"minecraft:big","minecraft:cow/variant":"minecraft:warm","minecraft:cow/sound_variant":"minecraft:classic","minecraft:chicken/variant":"minecraft:cold","minecraft:chicken/sound_variant":"minecraft:classic"}'),
('nautilus frog horse', '{"minecraft:zombie_nautilus/variant":"minecraft:warm","minecraft:frog/variant":"minecraft:cold","minecraft:horse/variant":"chestnut"}'),
('llama axolotl cat', '{"minecraft:llama/variant":"brown","minecraft:axolotl/variant":"blue","minecraft:cat/variant":"minecraft:tabby","minecraft:cat/sound_variant":"minecraft:classic","minecraft:cat/collar":"blue"}'),
('sheep shulker villager', '{"minecraft:sheep/color":"pink","minecraft:shulker/color":"purple","minecraft:villager/variant":"minecraft:taiga"}'),
('trim material direct provider', '{provides_trim_material:{palette_id:"minecraft:x",description:"X"}}'),
('everything at once', '{damage:3,unbreakable:{},custom_name:"n",enchantments:{"minecraft:mending":1},trim:{material:"minecraft:gold",pattern:"minecraft:coast"},banner_patterns:[{pattern:"minecraft:globe",color:"red"}],pot_decorations:{right:{id:"minecraft:brick"}},lore:["x"],rarity:"epic"}'),
]
# Fixtures on another item than a stick (the components need it, or read better there).
ITEMS = {'profile name': 'player_head', 'profile full': 'player_head', 'trim registry': 'diamond_chestplate',
'trim direct': 'diamond_chestplate', 'banner_patterns': 'white_banner', 'pot_decorations': 'decorated_pot',
'everything at once': 'diamond_chestplate'}
def compare(vanilla, rust):
"""Failures, stacks checked, and the component ids covered."""
results = {r['label']: r for r in rust}
failures, covered = [], set()
for v in vanilla:
if 'error' in v:
failures.append(f"oracle {v['label']}: {v['error']}")
continue
covered.update(v['added'] + v['removed'])
r = results.get(v['label'])
if r is None or 'error' in r:
failures.append(f"{v['label']}: {r and r['error']}")
elif r['left'] or r['added'] != v['added'] or r['removed'] != v['removed']:
failures.append(f"{v['label']}: {r['left']} bytes left, ids {r['added']}/{r['removed']}, vanilla {v['added']}/{v['removed']}")
elif not r['same_bytes'] and not r['typed']:
failures.append(f"{v['label']}: read past, but encoded back differently")
missing = sorted(set(range(COMPONENT_TYPES)) - covered)
if missing:
failures.append(f'component ids no stack covers: {missing}')
return failures, len(vanilla), covered
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--prepare', action='store_true')
args = parser.parse_args()
build = lab.LOCAL / 'item-wire-parity'
build.mkdir(parents=True, exist_ok=True)
report_path = build / 'report.json'
report_path.unlink(missing_ok=True)
if args.prepare:
lab.prepare()
lock, jars, libraries = base.verify_artifacts()
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaItemWireOracle.java'
classes = build / 'classes'
classes.mkdir(exist_ok=True)
def run(command, name):
with (build / (name + '.log')).open('w', encoding='utf-8') as log:
base.run(list(map(str, command)), stdout=log, stderr=subprocess.STDOUT)
dependency_path = os.pathsep.join(map(str, libraries))
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-sourcepath',
lab.ROOT / 'mods/horizon/src/test/java', '-cp', str(jars['server']) + os.pathsep + dependency_path,
'-d', classes, oracle], 'compile-java')
fixtures = build / 'fixtures.tsv'
fixtures.write_text(''.join(f'{label}\t{{id:"minecraft:{ITEMS.get(label, "stick")}",count:1,components:{snbt}}}\n'
for label, snbt in FIXTURES), encoding='utf-8')
vanilla_path, rust_path = build / 'vanilla.jsonl', build / 'rust.jsonl'
run(['java', '-Xmx1G', '-cp', os.pathsep.join([str(classes), str(jars['server']), dependency_path]),
'dev.horizonlab.parity.VanillaItemWireOracle', fixtures, vanilla_path], 'oracle')
run(['cargo', 'build', '--release', '-p', 'horizon-protocol', '--example', 'item_wire_probe'], 'compile-rust')
binary = lab.cargo_target() / 'release/examples' / ('item_wire_probe.exe' if os.name == 'nt' else 'item_wire_probe')
run([binary, vanilla_path, rust_path], 'rust')
read = lambda path: [json.loads(line) for line in path.read_text(encoding='utf-8').splitlines() if line.strip()]
failures, stacks, covered = compare(read(vanilla_path), read(rust_path))
for failure in failures[:40]:
print(failure)
if failures:
raise SystemExit(f'{len(failures)} item wire mismatches (of {stacks} stacks)')
lab.write_json(report_path, {
'schema': 1, 'minecraft': lock['minecraft']['version'], 'platform': platform.platform(),
'stacks': stacks, 'component_types_covered': len(covered),
'oracle_sha256': lab.digest(vanilla_path, 'sha256'), 'rust_sha256': lab.digest(rust_path, 'sha256')})
print(f'Item wire parity passed: {stacks} stacks, all {len(covered)} component types: {report_path}')
if __name__ == '__main__':
main()