diff --git a/crates/horizon-world/src/datafix/fixes_mid.rs b/crates/horizon-world/src/datafix/fixes_mid.rs index 54e6e67b9..54a4f51c0 100644 --- a/crates/horizon-world/src/datafix/fixes_mid.rs +++ b/crates/horizon-world/src/datafix/fixes_mid.rs @@ -177,6 +177,17 @@ pub(super) fn register(out: &mut Vec<(Key, FixFn)>) { ) })); out.push((key(2511, 0), projectile_owner)); + // `SplashPotionItemFieldRenameFix` casts the thrown potion to the type whose + // item stack field is `Item`: the typed `Potion` value takes its place, and + // writing the entity gives `Potion` back from the map it was read from. + out.push((key(2511, 1), |cx| { + cx.entity("minecraft:potion", |e| { + if let Some(item) = e.get("Potion").cloned() { + set(e, "Item", item); + } + super::drop_typed(e, "Potion"); + }) + })); out.push((key(2514, 0), entity_uuids)); out.push((key(2514, 0), block_entity_uuids)); out.push((key(2514, 0), player_uuids)); diff --git a/tools/datafix_scene.py b/tools/datafix_scene.py index 0f1861487..2afac5fcc 100644 --- a/tools/datafix_scene.py +++ b/tools/datafix_scene.py @@ -138,6 +138,9 @@ def experiments(version): def items(e, version=None): """The sample stacks of an era, in the exact component formats `version` accepts.""" stacks = ITEMS_A if e == A else ITEMS_B if e == B else ITEMS_C + if version is not None and version < 2586: # 1.13.2 to 1.15 + import datafix_scene_old + stacks = stacks + datafix_scene_old.items(version) if version is None: return stacks return [v for v in (versioned(s, version) for s in stacks) if available(v, version)] @@ -237,6 +240,9 @@ def block_entities(e, version): out = [(b, '{SpawnData:{id:"minecraft:zombie",ArmorItems:[{},{},{},{id:"minecraft:iron_helmet",Count:1b}]},' 'SpawnPotentials:[{Weight:1,Entity:{id:"minecraft:skeleton"}}]}') if b == 'spawner' else (b, nbt) for b, nbt in out] + if version < 2586: + import datafix_scene_old + out += datafix_scene_old.block_entities(version) if version >= 3682: out.append(('crafter', '{Items:[{Slot:0b,id:"minecraft:stone",Count:1b}],triggered:1,crafting_ticks_remaining:2}')) # Blocks the 1.20 experiment brought in 1.19.3 and 1.19.4. @@ -312,6 +318,9 @@ def entities(e, version): ] since = {'allay': 3105, 'chest_boat': 3105, 'text_display': 3325, 'item_display': 3325, 'goat': 2700} out = [(k, x) for k, x in out if version >= since.get(k, 0)] + if version < 2586: # 1.13.2 to 1.15: entities in the formats those versions write + import datafix_scene_old + out += datafix_scene_old.entities(version) if version < 3086: # cats before 1.19 have a numeric CatType out = [(k, 'CatType:1' if k == 'cat' else x) for k, x in out] if 3105 <= version < 3322: # darkness counts its factor from a timestamp before 1.19.4 @@ -363,6 +372,14 @@ def entities(e, version): return out +def blocks(version): + """Plain blocks whose states changed over the versions.""" + if version < 2586: + import datafix_scene_old + return datafix_scene_old.blocks(version) + return [] + + def player_commands(e, name, version=None): """Give the joined bot a varied inventory and ender chest.""" out = [] diff --git a/tools/datafix_scene_old.py b/tools/datafix_scene_old.py new file mode 100644 index 000000000..8cc310540 --- /dev/null +++ b/tools/datafix_scene_old.py @@ -0,0 +1,123 @@ +"""Scene additions for worlds saved by 1.13.2 to 1.16.5 (DataVersion 1631 to 2586): the entities, block +entities and items whose save format those versions changed, in the syntax and format of the version +that writes them (numeric UUIDMost/UUIDLeast, string owner UUIDs, Profession/Career villagers, ...). + +Every entry is (minimum DataVersion, maximum DataVersion exclusive, value); datafix_scene.py adds the +ones valid for the world's version to its general scene. +""" + +U1 = '11111111-2222-3333-4444-555555555555' +U2 = '66666666-7777-8888-9999-aaaaaaaaaaaa' +MODIFIERS = ('[{Name:"horse.jumpStrength",Base:0.7d},{Name:"generic.maxHealth",Base:20.0d,' + 'Modifiers:[{Name:"m",Amount:1.0d,Operation:0,UUIDMost:5L,UUIDLeast:6L}]}]') +TRADES = ('{buy:{id:"minecraft:wheat",Count:20b},sell:{id:"minecraft:emerald",Count:1b},maxUses:16,uses:1,rewardExp:1b},' + '{buy:{id:"minecraft:paper",Count:24b},sell:{id:"minecraft:emerald",Count:1b},maxUses:16,uses:0},' + '{buy:{id:"minecraft:book",Count:4b},sell:{id:"minecraft:emerald",Count:1b},maxUses:12,uses:0},' + '{buy:{id:"minecraft:ink_sac",Count:5b},sell:{id:"minecraft:emerald",Count:1b},maxUses:12,uses:0}') + +# (since, until, entity, fields) +ENTITIES = [ + # 1.13.2: professions are numbers + (1631, 1900, 'villager', 'Profession:1,Career:2,CareerLevel:2,Offers:{Recipes:[%s]},Inventory:[{id:"minecraft:bread",Count:3b}]' % TRADES), + (1631, 1900, 'villager', 'Profession:3,Career:3,CareerLevel:1'), + (1631, 1900, 'zombie_villager', 'Profession:4,Career:2'), + (1631, 1900, 'ocelot', 'CatType:2,OwnerUUID:"%s"' % U1), + (1631, 1900, 'ocelot', 'CatType:0'), + (1631, 1900, 'ocelot', 'CatType:0,OwnerUUID:"%s"' % U2), + # 1.14: VillagerData and Xp; some without them + (1900, 2100, 'villager', 'VillagerData:{profession:"minecraft:farmer",level:1,type:"minecraft:plains"},' + 'Offers:{Recipes:[%s]},Gossips:[{Type:"golem",Value:5},{Type:"major_positive",Value:2}]' % TRADES), + (1900, 2100, 'villager', 'VillagerData:{profession:"minecraft:armorer",level:3,type:"minecraft:desert"},Xp:70'), + (1900, 2100, 'zombie_villager', 'VillagerData:{profession:"minecraft:cleric",level:2,type:"minecraft:plains"}'), + (1900, 2100, 'cat', 'CatType:9'), + (1900, 2100, 'cat', 'CatType:3,CollarColor:5b'), + (1900, 2100, 'pillager', 'Inventory:[{id:"minecraft:arrow",Count:2b}]'), + (1900, 2100, 'fox', 'Type:"red",TrustedUUIDs:[{M:3L,L:4L}]'), + (1631, 2586, 'wolf', 'Owner:"%s",OwnerUUID:"%s",CollarColor:3b,Health:15.0f' % ('Steve', U1)), + (1631, 2586, 'horse', 'Tame:1b,OwnerUUID:"%s",SaddleItem:{id:"minecraft:saddle",Count:1b},' + 'ArmorItem:{id:"minecraft:iron_horse_armor",Count:1b},Attributes:%s' % (U2, MODIFIERS)), + (1631, 2586, 'armor_stand', 'Attributes:%s' % MODIFIERS), + (1631, 2586, 'cow', 'Leash:{UUIDMost:7L,UUIDLeast:8L}'), + (1631, 2586, 'zombie_pigman', 'Anger:100s,HurtBy:"%s"' % U1), + (1631, 2586, 'item', 'Item:{id:"minecraft:diamond",Count:3b},Owner:{M:9L,L:10L},Thrower:{M:11L,L:12L}'), + (1631, 2586, 'area_effect_cloud', 'OwnerUUIDMost:13L,OwnerUUIDLeast:14L,Particle:"entity_effect",Radius:2.0f,Duration:600'), + (1631, 2586, 'arrow', 'OwnerUUIDMost:15L,OwnerUUIDLeast:16L,inGround:1b'), + (1631, 2586, 'trident', 'OwnerUUIDMost:17L,OwnerUUIDLeast:18L,Trident:{id:"minecraft:trident",Count:1b}'), + (1631, 2586, 'potion', 'Potion:{id:"minecraft:splash_potion",Count:1b,tag:{Potion:"minecraft:swiftness"}},owner:{M:19L,L:20L}'), + (1631, 2586, 'snowball', 'owner:{M:21L,L:22L}'), + (1631, 2586, 'egg', 'owner:{M:23L,L:24L}'), + (1631, 2586, 'ender_pearl', 'owner:{M:25L,L:26L}'), + (1631, 2586, 'experience_bottle', 'owner:{M:27L,L:28L}'), + (1631, 2586, 'llama_spit', 'Owner:{OwnerUUIDMost:29L,OwnerUUIDLeast:30L}'), + (1631, 2586, 'shulker_bullet', 'Owner:{M:31L,L:32L},Target:{M:33L,L:34L}'), + (1631, 2586, 'evoker_fangs', 'OwnerUUIDMost:35L,OwnerUUIDLeast:36L'), + (1631, 2586, 'shulker', 'Rotation:[40.0f,0.0f],Peek:0b'), + (1631, 2586, 'bat', ''), + (2100, 2586, 'bee', 'HurtBy:"%s",HasNectar:1b,Anger:50' % U1), + (2100, 2586, 'villager', 'VillagerData:{profession:"minecraft:farmer",level:2,type:"minecraft:plains"},Xp:20,' + 'Brain:{memories:{"minecraft:job_site":{pos:[I;1,2,3],dimension:"minecraft:overworld"}}}'), + (2500, 2586, 'piglin', 'IsBaby:0b,Brain:{memories:{"minecraft:angry_at":"%s"}}' % U1), + (2500, 2586, 'strider', 'NoGravity:1b,Saddle:1b'), + (2500, 2586, 'zombified_piglin', 'HurtBy:"%s",Anger:60' % U2), + (2500, 2586, 'hoglin', 'IsImmuneToZombification:1b'), +] + +# (since, until, block, nbt) +BLOCK_ENTITIES = [ + (1631, 2586, 'white_banner', '{CustomName:\'{"translate":"block.minecraft.illager_banner","color":"gold"}\',' + 'Patterns:[{Pattern:"cre",Color:15}]}'), + (1631, 2586, 'chest', '{LootTable:"minecraft:chests/village_blacksmith"}'), + (1631, 2586, 'furnace', '{BurnTime:100s,CookTime:20s,CookTimeTotal:200s,RecipesUsedSize:2,RecipeLocation0:"minecraft:iron_ingot",' + 'RecipeAmount0:3,RecipeLocation1:"minecraft:glass",RecipeAmount1:1}'), + (1631, 2586, 'blast_furnace', '{RecipesUsedSize:1,RecipeLocation0:"minecraft:iron_ingot",RecipeAmount0:2}') if False else None, + (1631, 2586, 'player_head', '{Owner:{Id:"%s",Name:"Notch"}}' % U1), + (1631, 2586, 'conduit', '{target_uuid:{M:5L,L:6L},Target:{M:5L,L:6L}}'), + (1631, 2586, 'oak_sign', '{Text1:\'{"text":"one"}\',Text2:\'{"text":"two"}\'}'), + (1900, 2586, 'jigsaw[facing=up]', '{attachement_type:"minecraft:bottom",target_pool:"minecraft:village/plains/houses",' + 'final_state:"minecraft:air"}'), + (1900, 2586, 'jigsaw[facing=east]', '{attachement_type:"minecraft:empty",target_pool:"minecraft:empty"}'), + (1900, 2586, 'lectern', '{Book:{id:"minecraft:written_book",Count:1b,tag:{title:"L",author:"A",pages:[\'"x"\']}},Page:0}'), + (2100, 2586, 'beehive', '{Bees:[{EntityData:{id:"minecraft:bee"},TicksInHive:3,MinOccupationTicks:600}]}'), +] +BLOCK_ENTITIES = [b for b in BLOCK_ENTITIES if b] + +# Plain blocks whose states changed: (since, until, command tail after "setblock x y z") +BLOCKS = [ + (1900, 2500, 'cobblestone_wall[east=true,west=false,north=true,south=false]'), + (1900, 2500, 'andesite_wall[east=true,west=true,north=false,south=false,up=true]'), + (1631, 2500, 'redstone_wire[east=none,west=none,north=none,south=none,power=0]'), + (1631, 2500, 'redstone_wire[east=up,west=none,north=none,south=none,power=3]'), + (1631, 2500, 'redstone_wire[east=none,west=none,north=side,south=none,power=1]'), + (1631, 2500, 'oak_sign[rotation=3]'), + (1631, 1900, 'stone_slab[type=bottom]'), + (1631, 2100, 'bee_nest') if False else None, +] +BLOCKS = [b for b in BLOCKS if b] + +ITEMS = [ + (1631, 2586, '{id:"minecraft:white_banner",Count:1b,tag:{display:{Name:\'{"translate":"block.minecraft.illager_banner","color":"gold"}\',' + 'Lore:[\'first\',\'{"text":"second"}\']},BlockEntityTag:{Patterns:[{Pattern:"cre",Color:15}]}}}'), + (1631, 2586, '{id:"minecraft:player_head",Count:1b,tag:{SkullOwner:{Id:"%s",Name:"Notch"}}}' % U2), + (1631, 2586, '{id:"minecraft:diamond_sword",Count:1b,tag:{AttributeModifiers:[{AttributeName:"generic.attackDamage",Name:"x",Amount:2.5d,' + 'Operation:0,UUIDMost:41L,UUIDLeast:42L,Slot:"mainhand"}]}}'), + (1631, 2586, '{id:"minecraft:filled_map",Count:1b,tag:{map:0}}'), + (1631, 2586, '{id:"minecraft:cactus_green",Count:1b}') if False else None, + (1631, 2586, '{id:"minecraft:stone_slab",Count:2b}') if False else None, +] +ITEMS = [i for i in ITEMS if i] + + +def entities(version): + return [(kind, fields) for since, until, kind, fields in ENTITIES if since <= version < until] + + +def block_entities(version): + return [(block, nbt) for since, until, block, nbt in BLOCK_ENTITIES if since <= version < until] + + +def blocks(version): + return [block for since, until, block in BLOCKS if since <= version < until] + + +def items(version): + return [stack for since, until, stack in ITEMS if since <= version < until] diff --git a/tools/datafix_worlds.py b/tools/datafix_worlds.py index 6cae508c0..8b3533e3e 100644 --- a/tools/datafix_worlds.py +++ b/tools/datafix_worlds.py @@ -348,6 +348,13 @@ def uuid_nbt(i): return f'[I;{UUID_BASE},0,0,{i}]' +def uuid_fields(data_version, i): + """The UUID of a summoned entity: an int array since 1.16, two longs before.""" + if data_version >= 2514: + return f'UUID:{uuid_nbt(i)}' + return f'UUIDMost:{UUID_BASE << 32}L,UUIDLeast:{i + 1}L' + + def build(version, force=False): jar, data_version, protocol = server_jar(version) target = SHARED / 'worlds' / version @@ -376,8 +383,10 @@ def build(version, force=False): run(f'setblock {x} 100 {z} minecraft:{block}{nbt}') for i, (kind, extra) in enumerate(scene.entities(era, data_version)): x, z = 1.5 + 3 * (i % 15), 25.5 + 3 * (i // 15) - fields = f'UUID:{uuid_nbt(i)},NoAI:1b,Silent:1b,NoGravity:1b,Invulnerable:1b,PersistenceRequired:1b' + fields = f'{uuid_fields(data_version, i)},NoAI:1b,Silent:1b,NoGravity:1b,Invulnerable:1b,PersistenceRequired:1b' run(f'summon minecraft:{kind} {x} 100 {z} {{{fields}' + (',' + extra if extra else '') + '}') + for i, block in enumerate(scene.blocks(data_version)): + run(f'setblock {1 + 3 * (i % 15)} 100 {19 + 3 * (i // 15)} minecraft:{block}') run('setworldspawn 20 101 20') bot = Bot(port, protocol, BOT) time.sleep(3)