Files
horizon-airdef/tools/save_roundtrip_scene.py
Ludovic Marie a903cb4dd5 Pass the save round-trip harness
Horizon spawned mobs whatever the spawn_mobs and spawn_monsters game rules
said; natural spawning now follows ServerChunkCache.tickChunks (the chunks
are still shuffled every tick) and the custom spawners tick only with
spawn_mobs. The slimes this put in the test world were what woke its sculk
sensor.

Falling blocks keep their UUID and loaded compound while they move, read
NoGravity, DropItem and a signed Time, and drop themselves as an item
after 600 ticks off the ground instead of landing. A crafter's saved
triggered flag only follows the block state when the state changes
(CrafterBlock.neighborChanged), and a horse saves the attribute instances
it was loaded with, as AttributeMap.pack does.

The harness compares vanilla log lines without their time stamps, accepts
extra entities only in chunks vanilla never generated, treats a falling
block's Time as a timer, and keeps the lit furnace's slots out of the
vanilla view. Its scene starts the falling block far below the 600-tick
limit and keeps the campfire from finishing during the run. The known-gap
list is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:49:38 +02:00

134 lines
9.3 KiB
Python

"""The test arrangement of tools/test_save_roundtrip_parity.py: one of every vanilla 26.3
block entity type and many entity types, placed by console commands in a flat world.
BLOCK_ENTITIES entries are (block id and states, block entity NBT in SNBT or '').
ENTITIES entries are (entity id, extra SNBT fields). Entities get deterministic UUIDs
and NoAI/Silent/NoGravity so that the two saves can be matched one to one.
"""
SWORD = ('{Slot:0b,id:"minecraft:iron_sword",count:1,components:{"minecraft:damage":7,'
'"minecraft:custom_name":{text:"Blade",italic:0b},"minecraft:enchantments":{"minecraft:sharpness":3}}}')
STONE = '{Slot:5b,id:"minecraft:stone",count:64}'
APPLE = '{Slot:1b,id:"minecraft:apple",count:3}'
NAME = 'CustomName:{text:"Named",color:"red"}'
LOCKED = 'lock:{components:{"minecraft:custom_name":{text:"Key"}}}'
LOOT = 'LootTable:"minecraft:chests/simple_dungeon",LootTableSeed:12345L'
SIGN_SIDE = ('{messages:[{text:"one",color:"red"},"two",{text:"three",bold:1b},""],color:"blue",'
'has_glowing_text:1b}')
BOOK = ('{id:"minecraft:written_book",count:1,components:{"minecraft:written_book_content":'
'{title:{raw:"Title"},author:"Author",pages:[{raw:"page one"},{raw:"page two"}]}}}')
FULL = [
('chest', f'{{Items:[{SWORD},{STONE}],{NAME},{LOCKED}}}'),
('chest[facing=east]', f'{{{LOOT}}}'),
('trapped_chest', f'{{Items:[{SWORD},{APPLE}]}}'),
('ender_chest', ''),
('barrel[facing=up]', f'{{Items:[{STONE}],{NAME}}}'),
('barrel[facing=up]', f'{{{LOOT}}}'),
('dispenser[facing=up]', f'{{Items:[{APPLE}],{NAME}}}'),
('dropper[facing=up]', f'{{Items:[{STONE}]}}'),
('hopper', f'{{Items:[{STONE}],TransferCooldown:3,{NAME}}}'),
('red_shulker_box', f'{{Items:[{SWORD},{STONE}],{NAME}}}'),
('furnace', f'{{Items:[{{Slot:0b,id:"minecraft:iron_ore",count:5}},{{Slot:1b,id:"minecraft:coal",count:2}}],'
'lit_time_remaining:100,lit_total_time:200,cooking_time_spent:50,cooking_total_time:200,'
'speed_multiplier:1.5f,RecipesUsed:{"minecraft:iron_ingot":2}}'),
('blast_furnace', '{Items:[{Slot:2b,id:"minecraft:iron_ingot",count:9}],RecipesUsed:{"minecraft:copper_ingot":4}}'),
('smoker', f'{{{NAME},cooking_total_time:100}}'),
('brewing_stand', f'{{Items:[{{Slot:0b,id:"minecraft:potion",count:1}},{{Slot:3b,id:"minecraft:nether_wart",count:2}},'
'{Slot:4b,id:"minecraft:blaze_powder",count:1}],BrewTime:200,Fuel:10}'),
# The beef must not finish cooking while the scene runs: its item would land
# near the sculk sensors at a time that depends on how long each game ran.
('campfire', '{Items:[{Slot:0b,id:"minecraft:beef",count:1}],CookingTimes:[I;10,0,0,0],'
'CookingTotalTimes:[I;60000,0,0,0]}'),
('soul_campfire', ''),
('chiseled_bookshelf', '{Items:[{Slot:0b,id:"minecraft:book",count:1},{Slot:3b,id:"minecraft:book",count:1}],'
'last_interacted_slot:3}'),
('oak_shelf', f'{{Items:[{APPLE}],align_items_to_bottom:1b}}'),
('crafter', f'{{crafting_ticks_remaining:3,disabled_slots:[I;2,4],triggered:1,Items:[{APPLE}]}}'),
('decorated_pot', '{sherds:{back:"minecraft:brick",left:"minecraft:brick",right:"minecraft:angler_pottery_sherd",'
'front:{id:"minecraft:brick"}},item:{id:"minecraft:diamond",count:2}}'),
('suspicious_sand', '{item:{id:"minecraft:diamond",count:1}}'),
('suspicious_gravel', f'{{{LOOT}}}'),
('jukebox', '{RecordItem:{id:"minecraft:music_disc_13",count:1},ticks_since_song_started:5L}'),
('lectern', f'{{Book:{BOOK},Page:1}}'),
('beacon', f'{{Levels:0,primary_effect:"minecraft:speed",secondary_effect:"minecraft:regeneration",{NAME}}}'),
('bell', ''),
('white_banner', '{patterns:[{pattern:"minecraft:stripe_top",color:"red"},'
'{pattern:"minecraft:creeper",color:"black"}],CustomName:{text:"Flag"}}'),
('skeleton_skull', '{note_block_sound:"minecraft:block.note_block.imitate.skeleton",custom_name:{text:"Skully"}}'),
('creeper_head', ''),
('oak_sign', f'{{front_text:{SIGN_SIDE},back_text:{{messages:["a","b","c","d"],color:"black",has_glowing_text:0b}},is_waxed:1b}}'),
('oak_hanging_sign', f'{{front_text:{SIGN_SIDE}}}'),
('spawner', '{SpawnData:{entity:{id:"minecraft:pig"}},Delay:20s,MinSpawnDelay:200s,MaxSpawnDelay:800s,'
'SpawnCount:4s,MaxNearbyEntities:6s,RequiredPlayerRange:16s,SpawnRange:4s}'),
('trial_spawner', '{required_player_range:10,target_cooldown_length:1200}'),
('vault', '{config:{loot_table:"minecraft:chests/trial_chambers/reward",key_item:{id:"minecraft:trial_key",count:1}},'
'server_data:{rewarded_players:[[I;1,2,3,4]],total_ejections_needed:2}}'),
('command_block', f'{{Command:"say hi",auto:1b,TrackOutput:0b,{NAME}}}'),
('chain_command_block', '{Command:"say chain",conditionMet:1b}'),
('comparator', '{OutputSignal:5}'),
('conduit', ''),
('daylight_detector', ''),
('enchanting_table', f'{{{NAME}}}'),
('end_gateway', '{Age:20L,exit_portal:[I;0,70,0],ExactTeleport:1b}'),
('end_portal', ''),
('jigsaw', '{name:"minecraft:a",target:"minecraft:b",pool:"minecraft:empty",final_state:"minecraft:stone",'
'joint:"aligned",placement_priority:1,selection_priority:2}'),
('structure_block', '{name:"test:s",author:"me",metadata:"meta",mode:"SAVE",posX:1,posY:2,posZ:3,sizeX:4,sizeY:5,'
'sizeZ:6,rotation:"CLOCKWISE_90",mirror:"LEFT_RIGHT",ignoreEntities:0b,integrity:0.5f,seed:9L}'),
('sculk_sensor', '{last_vibration_frequency:3}'),
('calibrated_sculk_sensor', ''),
('sculk_catalyst', ''),
('sculk_shrieker[can_summon=true]', '{warning_level:2}'),
('creaking_heart', ''),
('beehive', '{bees:[{entity_data:{id:"minecraft:bee"},ticks_in_hive:100,min_ticks_in_hive:2400}],flower_pos:[I;1,2,3]}'),
('bee_nest', ''),
('test_block', '{mode:"accept",message:"hello",powered:1b}'),
('test_instance_block', ''),
('copper_golem_statue', ''),
('potent_sulfur', '{countdown:7}'),
]
# The same blocks with no NBT: vanilla fills every field with its default, which
# Horizon must reproduce when it loads and saves them.
BLOCK_ENTITIES = FULL + [(block, '') for block, nbt in FULL if nbt]
# (entity id, extra SNBT). Each also gets UUID, Pos, NoAI, Silent, a name and a tag.
ENTITIES = [
('pig', ''), ('cow', 'Age:-100'), ('sheep', 'Color:5b,Sheared:1b'), ('chicken', 'EggLayTime:1000'),
('horse', 'Tame:1b,Variant:258,Temper:30'), ('wolf', 'Health:12f,CollarColor:3b'), ('cat', ''),
('villager', 'VillagerData:{profession:"minecraft:farmer",level:2,type:"minecraft:plains"},Xp:15,'
'Offers:{Recipes:[{buy:{id:"minecraft:wheat",count:20},sell:{id:"minecraft:emerald",count:1},'
'maxUses:16,uses:2,xp:2,priceMultiplier:0.05f,demand:0}]}'),
('zombie', 'IsBaby:1b,ArmorItems:[{},{},{},{id:"minecraft:iron_helmet",count:1}],HandItems:[{id:"minecraft:iron_sword",count:1},{}]'),
('skeleton', ''), ('creeper', 'powered:1b,Fuse:20s'), ('spider', ''), ('enderman', ''),
('bee', 'HasNectar:1b,Anger:0'), ('axolotl', 'Variant:2'), ('frog', ''), ('fox', 'Type:"snow"'),
('parrot', 'Variant:3'), ('llama', 'Strength:3,Variant:1'), ('rabbit', 'RabbitType:2'),
('allay', ''), ('iron_golem', 'PlayerCreated:1b'), ('snow_golem', 'Pumpkin:0b'),
('armor_stand', 'ShowArms:1b,Small:1b,Pose:{Head:[10f,20f,30f]},ArmorItems:[{},{},{},{id:"minecraft:diamond_helmet",count:1}]'),
('item', 'Item:{id:"minecraft:diamond",count:5},Age:100,PickupDelay:20s'),
('experience_orb', 'Value:7s,Age:30s'),
('item_frame', 'Facing:3b,Item:{id:"minecraft:apple",count:1},ItemRotation:2b,ItemDropChance:0.5f'),
('glow_item_frame', 'Facing:2b'),
('painting', 'Facing:3b,variant:"minecraft:kebab"'),
('arrow', 'inGround:1b,life:5s,damage:2.0d'),
# Time starts far below 600, after which FallingBlockEntity drops itself as an item.
('falling_block', 'BlockState:{id:"minecraft:anvil",properties:{facing:"east"}},Time:-100000,HurtEntities:1b,FallHurtAmount:2f'),
('tnt', 'fuse:50s,block_state:{id:"minecraft:tnt",properties:{unstable:"true"}}'),
('oak_boat', ''), ('chest_boat', ''),
('minecart', ''), ('chest_minecart', 'Items:[{Slot:0b,id:"minecraft:apple",count:2}]'),
('hopper_minecart', ''), ('furnace_minecart', 'Fuel:100s'), ('tnt_minecart', 'TNTFuse:-1'),
('text_display', 'text:{text:"Hello"},billboard:"center",line_width:100,background:1234,alignment:"left"'),
('block_display', 'block_state:{id:"minecraft:oak_stairs",properties:{facing:"east"}},transformation:{translation:[1f,0f,0f],left_rotation:[0f,0f,0f,1f],right_rotation:[0f,0f,0f,1f],scale:[2f,2f,2f]}'),
('item_display', 'item:{id:"minecraft:stone",count:1},item_display:"head"'),
('interaction', 'width:2f,height:3f,response:1b'),
('marker', 'data:{a:1,b:"two"}'),
('area_effect_cloud', 'Radius:3f,Duration:200,Particle:{type:"minecraft:entity_effect",color:16711680},Potion:"minecraft:poison"'),
('end_crystal', 'ShowBottom:0b'),
('snowball', ''), ('ender_pearl', ''), ('small_fireball', ''), ('wither_skull', 'dangerous:1b'),
('glow_squid', ''), ('turtle', 'HomePosX:1,HomePosY:-60,HomePosZ:1,HasEgg:1b'),
('ghast', ''), ('piglin', ''), ('hoglin', ''), ('strider', 'Saddle:1b'), ('warden', ''),
('trader_llama', ''), ('mooshroom', 'Type:"brown"'), ('panda', 'MainGene:"lazy"'),
('copper_golem', ''), ('creaking', ''),
]