874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
154 lines
10 KiB
Python
154 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare block entity data on items with vanilla 26.3.
|
|
|
|
The Java oracle (VanillaPickBlockDataOracle) loads each block entity with
|
|
BlockEntity.loadStatic and runs ServerGamePacketListenerImpl.addBlockDataToItem
|
|
(Ctrl + Pick Block in creative) on the block's item, then places an item as a
|
|
block entity: BlockItem.updateCustomBlockEntityTag (op-only types only for a
|
|
game master) and BlockEntity.applyComponentsFromItemStack. The Rust example
|
|
(pick_block_data_fixture) runs horizon_client::block_entity_item on the
|
|
canonical saved NBT. The picked component patch and the placed block entity's
|
|
saveWithFullMetadata are compared for every block entity type that carries data:
|
|
containers with items, names, locks and loot tables, signs, banners, command
|
|
blocks, spawners, trial spawners, vaults, beehives, pots, skulls, lecterns and
|
|
more, picked and placed back, with hand-written patches (data of another type,
|
|
op-only data without permission, merges into a filled block entity, leftover
|
|
components).
|
|
"""
|
|
import argparse
|
|
import json
|
|
try:
|
|
from . import interaction_lab as lib
|
|
except ImportError:
|
|
import interaction_lab as lib
|
|
|
|
POS = 'x:0,y:64,z:0'
|
|
STONE = '{id:"minecraft:stone",count:5}'
|
|
NAME = 'CustomName:"Box"'
|
|
PIG = 'SpawnData:{entity:{id:"minecraft:pig"}}'
|
|
# A block entity that spawns from nothing: what loading it saves (a spawner's
|
|
# potentials and a trial spawner's next spawn come from its first spawn data).
|
|
FRESH = {'mob_spawner': PIG, 'trial_spawner': 'spawn_data:{entity:{}}'}
|
|
|
|
|
|
def be(type_id, body=''):
|
|
return '{id:"minecraft:%s",%s%s}' % (type_id, POS, ',' + body if body else '')
|
|
|
|
|
|
def items(*slots):
|
|
return 'Items:[%s]' % ','.join('{Slot:%db,id:"minecraft:%s",count:%d}' % s for s in slots)
|
|
|
|
|
|
# (block, block entity type, saved NBT bodies to pick and place back).
|
|
SAMPLES = [
|
|
('chest', 'chest', ['', items((0, 'stone', 5), (26, 'diamond', 1)), NAME, 'lock:{items:"minecraft:diamond"}',
|
|
'LootTable:"minecraft:chests/simple_dungeon",LootTableSeed:42L',
|
|
'LootTable:"minecraft:chests/igloo_chest"', items((3, 'dirt', 1)) + ',' + NAME]),
|
|
('trapped_chest', 'trapped_chest', [items((1, 'stone', 2))]),
|
|
('barrel', 'barrel', [items((5, 'apple', 3)) + ',' + NAME]),
|
|
('red_shulker_box', 'shulker_box', ['', items((0, 'stone', 64), (13, 'emerald', 2)) + ',' + NAME]),
|
|
('hopper', 'hopper', ['TransferCooldown:5', items((4, 'stone', 1)) + ',TransferCooldown:2']),
|
|
('dispenser', 'dispenser', [items((8, 'arrow', 16))]),
|
|
('dropper', 'dropper', [items((0, 'stone', 1)) + ',' + NAME]),
|
|
('crafter', 'crafter', ['disabled_slots:[I;1,4],triggered:1,crafting_ticks_remaining:3',
|
|
items((2, 'stick', 1)) + ',disabled_slots:[I;0]']),
|
|
('furnace', 'furnace', ['cooking_time_spent:20,cooking_total_time:200,lit_time_remaining:100,lit_total_time:1600,'
|
|
+ items((0, 'iron_ore', 3), (1, 'coal', 4)) + ',RecipesUsed:{"minecraft:iron_ingot":2}']),
|
|
('smoker', 'smoker', [items((2, 'cooked_beef', 1))]),
|
|
('blast_furnace', 'blast_furnace', [NAME + ',lit_time_remaining:7']),
|
|
('brewing_stand', 'brewing_stand', ['BrewTime:30,Fuel:10,' + items((4, 'blaze_powder', 1))]),
|
|
# Levels is computed by the beacon's tick, never loaded.
|
|
('beacon', 'beacon', ['primary_effect:"minecraft:speed",secondary_effect:"minecraft:regeneration"',
|
|
NAME + ',lock:{items:"minecraft:stick"}']),
|
|
('enchanting_table', 'enchanting_table', ['', NAME]),
|
|
('command_block', 'command_block', ['Command:"say hi",auto:1b,powered:1b,conditionMet:1b,TrackOutput:0b,SuccessCount:3',
|
|
'Command:"time set day",' + NAME, 'LastOutput:"{\\"text\\":\\"x\\"}"',
|
|
'UpdateLastExecution:0b,LastExecution:20L']),
|
|
('repeating_command_block', 'command_block', ['Command:"say loop",auto:1b']),
|
|
('campfire', 'campfire', [items((0, 'beef', 1), (3, 'porkchop', 1)) + ',CookingTimes:[I;5,0,0,9],CookingTotalTimes:[I;600,0,0,600]']),
|
|
('chiseled_bookshelf', 'chiseled_bookshelf', [items((0, 'book', 1), (5, 'enchanted_book', 1)) + ',last_interacted_slot:5']),
|
|
('oak_shelf', 'shelf', [items((1, 'stone', 1)) + ',align_items_to_bottom:1b']),
|
|
('beehive', 'beehive', ['', 'bees:[{entity_data:{id:"minecraft:bee"},ticks_in_hive:5,min_ticks_in_hive:600}]',
|
|
'flower_pos:[I;1,2,3]']),
|
|
('white_banner', 'banner', ['', 'patterns:[{pattern:"minecraft:creeper",color:"red"}]',
|
|
'patterns:[{pattern:"minecraft:stripe_top",color:"blue"}],' + NAME]),
|
|
('decorated_pot', 'decorated_pot', ['', 'sherds:{back:{id:"minecraft:angler_pottery_sherd"},front:{id:"minecraft:skull_pottery_sherd"}}',
|
|
'item:{id:"minecraft:diamond",count:3}', 'LootTable:"minecraft:archaeology/desert_pyramid"']),
|
|
('oak_sign', 'sign', ['', 'front_text:{messages:["hello","world","",""]}',
|
|
'front_text:{messages:["a","","",""],color:"red",has_glowing_text:1b},back_text:{messages:["","b","",""]},is_waxed:1b']),
|
|
('oak_hanging_sign', 'hanging_sign', ['back_text:{messages:["x","","",""]}']),
|
|
('skeleton_skull', 'skull', ['', 'custom_name:"Bones"', 'note_block_sound:"minecraft:block.note_block.bell"']),
|
|
('spawner', 'mob_spawner', ['Delay:20,SpawnData:{entity:{id:"minecraft:zombie"}},MinSpawnDelay:200,MaxSpawnDelay:800',
|
|
'SpawnData:{entity:{id:"minecraft:pig"}},SpawnCount:2,RequiredPlayerRange:8']),
|
|
('trial_spawner', 'trial_spawner', ['spawn_data:{entity:{id:"minecraft:zombie"}}',
|
|
'normal_config:"minecraft:trial_chamber/melee/zombie/normal",required_player_range:10,spawn_data:{entity:{}}']),
|
|
('vault', 'vault', ['', 'config:{key_item:{id:"minecraft:ominous_trial_key",count:1}}']),
|
|
('jukebox', 'jukebox', ['', 'RecordItem:{id:"minecraft:music_disc_cat",count:1},ticks_since_song_started:40L']),
|
|
('lectern', 'lectern', ['', 'Book:{id:"minecraft:writable_book",count:1,components:{"minecraft:writable_book_content":{pages:[{raw:"a"},{raw:"b"}]}}},Page:1']),
|
|
('sculk_sensor', 'sculk_sensor', ['last_vibration_frequency:4']),
|
|
('creaking_heart', 'creaking_heart', ['']),
|
|
('bell', 'bell', ['']),
|
|
('structure_block', 'structure_block', ['name:"minecraft:house",mode:"SAVE",posY:2']),
|
|
]
|
|
|
|
# Patches placed as (block, block entity type, target body, patch SNBT, game master).
|
|
DATA = '"minecraft:block_entity_data"'
|
|
PATCHES = [
|
|
('command_block', 'command_block', '', '{%s:{id:"minecraft:command_block",Command:"say op"}}' % DATA, False),
|
|
('command_block', 'command_block', '', '{%s:{id:"minecraft:chest",Command:"say wrong"}}' % DATA, True),
|
|
('command_block', 'command_block', 'Command:"old",auto:1b', '{%s:{id:"minecraft:command_block",TrackOutput:0b}}' % DATA, True),
|
|
('oak_sign', 'sign', '', '{%s:{id:"minecraft:sign",is_waxed:1b}}' % DATA, False),
|
|
('oak_sign', 'sign', '', '{%s:{id:"minecraft:sign",is_waxed:1b}}' % DATA, True),
|
|
('chest', 'chest', items((0, 'stone', 1)), '{%s:{id:"minecraft:chest",Items:[{Slot:1b,id:"minecraft:dirt",count:1}]}}' % DATA, True),
|
|
('chest', 'chest', '', '{"minecraft:container":[{slot:30,item:%s},{slot:2,item:%s}]}' % (STONE, STONE), False),
|
|
('chest', 'chest', '', '{"minecraft:container_loot":{loot_table:"minecraft:chests/simple_dungeon",seed:7L}}', False),
|
|
('chest', 'chest', '', '{"minecraft:lore":["a"],"minecraft:custom_name":"Named"}', False),
|
|
('hopper', 'hopper', 'TransferCooldown:4', '{%s:{id:"minecraft:hopper",TransferCooldown:9}}' % DATA, False),
|
|
('spawner', 'mob_spawner', PIG, '{%s:{id:"minecraft:mob_spawner",SpawnData:{entity:{id:"minecraft:skeleton"}}}}' % DATA, True),
|
|
('spawner', 'mob_spawner', PIG, '{%s:{id:"minecraft:mob_spawner",SpawnData:{entity:{id:"minecraft:skeleton"}}}}' % DATA, False),
|
|
('trial_spawner', 'trial_spawner', 'spawn_data:{entity:{}}', '{%s:{id:"minecraft:trial_spawner",required_player_range:4}}' % DATA, True),
|
|
('vault', 'vault', '', '{%s:{id:"minecraft:vault",config:{key_item:{id:"minecraft:stick",count:1}}}}' % DATA, False),
|
|
('decorated_pot', 'decorated_pot', '', '{"minecraft:container":[{slot:1,item:%s}]}' % STONE, False),
|
|
('white_banner', 'banner', NAME, '{"minecraft:banner_patterns":[{pattern:"minecraft:flower",color:"lime"}]}', False),
|
|
('beehive', 'beehive', 'bees:[{entity_data:{id:"minecraft:bee"},ticks_in_hive:1,min_ticks_in_hive:2}]', '{}', False),
|
|
('decorated_pot', 'decorated_pot', '', '{"!minecraft:pot_decorations":{}}', False),
|
|
('hopper', 'hopper', items((0, 'stone', 1)), '{"minecraft:container":[{slot:4,item:%s},{slot:5,item:%s}]}' % (STONE, STONE), False),
|
|
('oak_sign', 'sign', '', '{"minecraft:waxed":{},"minecraft:sign_text_back":{messages:["","","","z"]}}', False),
|
|
]
|
|
|
|
|
|
def cases():
|
|
out = []
|
|
for block, type_id, bodies in SAMPLES:
|
|
for body in bodies:
|
|
for op in (True, False):
|
|
out.append({'block': 'minecraft:' + block, 'nbt': be(type_id, body),
|
|
'target': be(type_id, FRESH.get(type_id, '')), 'op': op})
|
|
for block, type_id, target, patch, op in PATCHES:
|
|
out.append({'block': 'minecraft:' + block, 'nbt': be(type_id, FRESH.get(type_id, '')), 'target': be(type_id, target), 'patch': patch, 'op': op})
|
|
return out
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--limit', type=int, default=4)
|
|
args = parser.parse_args()
|
|
work = lib.work_dir('pick-block-data-parity')
|
|
vanilla = lib.Vanilla(work)
|
|
vanilla.compile('ParityData', 'VanillaPickBlockDataOracle')
|
|
all_cases = cases()
|
|
(work / 'cases.jsonl').write_text(''.join(json.dumps(c) + '\n' for c in all_cases))
|
|
vanilla.run('VanillaPickBlockDataOracle', work / 'cases.jsonl', work / 'native.txt')
|
|
lib.cargo_example('horizon-client', 'pick_block_data_fixture', work / 'cases.jsonl', work / 'rust.txt')
|
|
errors = sum(line == 'error' for line in (work / 'native.txt').read_text().split('\n'))
|
|
if errors:
|
|
print(f'{errors} cases fail in vanilla')
|
|
total = lib.compare(all_cases, work / 'native.txt', work / 'rust.txt',
|
|
lambda case, line: case['block'] + (' patch' if 'patch' in case else ''),
|
|
lambda case: json.dumps(case), args.limit)
|
|
raise SystemExit(1 if total or errors else 0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|