9223b401e7
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>
178 lines
14 KiB
Python
Executable File
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()
|