Files
RustNotMC/tools/datafix_scene_12.py
T
SashegDev 874b6f20c2 Клиент: начальный импорт кодовой базы
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
2026-10-29 22:04:28 +03:00

1933 lines
110 KiB
Python

"""The 1.12.2 scene for tools/datafix_worlds.py: a rich sample world for the DataFixers of
1.13 (DataVersion 1343 to 1631), written in the command syntax of 1.12.
The scene is a set of 96 x 96 stone platforms ("layers", one per chunk section: y = 100, 116 .. 244)
above the spawn area. Every block family of the 1.12 id/data space sits on its own cells, one empty cell
between neighbours (so that no neighbour update rewrites a state), with the tile entities, items,
entities, player data, scoreboard, maps, advancements and statistics that the flattening touches.
States that the game recomputes at once (powered pistons, repeaters, fluids, hanging entities ...) are
placed in the very last tick, together with `save-all flush`, and the server is killed right after: the saved chunks keep them.
populate(server, Bot, port, protocol, name, problems) -> runs everything, bot joined, returns the bot
What a real server cannot produce is written by hand after the stop, see `fixtures()` (marked there).
"""
import json
import queue
import re
import shutil
import socket
import struct
import sys
import threading
import time
from pathlib import Path
import lab
BX, BZ, SIZE = 144, 144, 96 # south-west corner and size of the platforms (inside the spawn chunks)
LAYERS = list(range(100, 245, 16)) # platform y levels: one chunk section each, the last one reaches y = 255
UUID_HI = 0x6000
# ------------------------------------------------------------------ versions
# One builder for 1.8.9 to 1.12.2; `ERA` is the minor version (8 to 12), set by `set_protocol` from the bot's protocol.
# 1.8: numeric item ids in saves, CamelCase entity and tile entity ids, `Equipment`, `Riding`
# 1.9: `ArmorItems`/`HandItems`, `Passengers`, shulkers, lingering potions, tipped arrows, elytra, chorus, purpur ...
# 1.10: husks and strays (zombie / skeleton types), polar bears, magma, bone blocks, nether wart blocks
# 1.11: namespaced entity ids, evokers, vindicators, vexes, llamas, separate donkeys and so on, shulker boxes, observers
# 1.12: parrots, illusioners, concrete, glazed terracotta, advancements instead of achievements
ERA = 12
PROTOCOL_ERA = {47: 8, 107: 9, 108: 9, 109: 9, 110: 9, 210: 10, 315: 11, 316: 11, 335: 12, 338: 12, 340: 12}
VALID_ITEMS = None # item names the server accepts (probed at the start), None: everything
def set_protocol(protocol):
global ERA
ERA = PROTOCOL_ERA[protocol]
def block_ok(name):
"""Whether this version has the block (by its numeric id)."""
i = BLOCK_IDS.get(name)
if i is None:
return False
limit = {8: 197, 9: 212, 10: 217, 11: 234, 12: 255}[ERA]
return i <= limit or (i == 255 and ERA >= 9)
# ------------------------------------------------------------------ tables
# `id:name=data,ranges` of every 1.12 block state (from vanilla's BlockStateData), id = numeric block id.
BLOCKS = """
0:air=0 1:stone=0-6 2:grass=0 3:dirt=0-2 4:cobblestone=0 5:planks=0-5 6:sapling=0-5,8-13 7:bedrock=0
8:flowing_water=0-15 9:water=0-15 10:flowing_lava=0-15 11:lava=0-15 12:sand=0-1 13:gravel=0 14:gold_ore=0
15:iron_ore=0 16:coal_ore=0 17:log=0-15 18:leaves=0-15 19:sponge=0-1 20:glass=0 21:lapis_ore=0 22:lapis_block=0
23:dispenser=0-5,8-13 24:sandstone=0-2 25:noteblock=0 26:bed=0-3,8-15 27:golden_rail=0-5,8-13
28:detector_rail=0-5,8-13 29:sticky_piston=0-5,8-13 30:web=0 31:tallgrass=0-2 32:deadbush=0 33:piston=0-5,8-13
34:piston_head=0-5,8-13 35:wool=0-15 36:piston_extension=0-5,8-13 37:yellow_flower=0 38:red_flower=0-8
39:brown_mushroom=0 40:red_mushroom=0 41:gold_block=0 42:iron_block=0 43:double_stone_slab=0-15 44:stone_slab=0-15
45:brick_block=0 46:tnt=0-1 47:bookshelf=0 48:mossy_cobblestone=0 49:obsidian=0 50:torch=1-5 51:fire=0-15
52:mob_spawner=0 53:oak_stairs=0-7 54:chest=2-5 55:redstone_wire=0-15 56:diamond_ore=0 57:diamond_block=0
58:crafting_table=0 59:wheat=0-7 60:farmland=0-7 61:furnace=2-5 62:lit_furnace=2-5 63:standing_sign=0-15
64:wooden_door=0-11 65:ladder=2-5 66:rail=0-9 67:stone_stairs=0-7 68:wall_sign=2-5 69:lever=0-15
70:stone_pressure_plate=0-1 71:iron_door=0-11 72:wooden_pressure_plate=0-1 73:redstone_ore=0 74:lit_redstone_ore=0
75:unlit_redstone_torch=1-5 76:redstone_torch=1-5 77:stone_button=0-5,8-13 78:snow_layer=0-7 79:ice=0 80:snow=0
81:cactus=0-15 82:clay=0 83:reeds=0-15 84:jukebox=0-1 85:fence=0 86:pumpkin=0-3 87:netherrack=0 88:soul_sand=0
89:glowstone=0 90:portal=1-2 91:lit_pumpkin=0-3 92:cake=0-6 93:unpowered_repeater=0-15 94:powered_repeater=0-15
95:stained_glass=0-15 96:trapdoor=0-15 97:monster_egg=0-5 98:stonebrick=0-3 99:brown_mushroom_block=0-10,14-15
100:red_mushroom_block=0-10,14-15 101:iron_bars=0 102:glass_pane=0 103:melon_block=0 104:pumpkin_stem=0-7
105:melon_stem=0-7 106:vine=0-15 107:fence_gate=0-15 108:brick_stairs=0-7 109:stone_brick_stairs=0-7 110:mycelium=0
111:waterlily=0 112:nether_brick=0 113:nether_brick_fence=0 114:nether_brick_stairs=0-7 115:nether_wart=0-3
116:enchanting_table=0 117:brewing_stand=0-7 118:cauldron=0-3 119:end_portal=0 120:end_portal_frame=0-7
121:end_stone=0 122:dragon_egg=0 123:redstone_lamp=0 124:lit_redstone_lamp=0 125:double_wooden_slab=0-5
126:wooden_slab=0-5,8-13 127:cocoa=0-11 128:sandstone_stairs=0-7 129:emerald_ore=0 130:ender_chest=2-5
131:tripwire_hook=0-15 132:tripwire=0-1,4-5,8-9,12-13 133:emerald_block=0 134:spruce_stairs=0-7 135:birch_stairs=0-7
136:jungle_stairs=0-7 137:command_block=0-5,8-13 138:beacon=0 139:cobblestone_wall=0-1 140:flower_pot=0-15
141:carrots=0-7 142:potatoes=0-7 143:wooden_button=0-5,8-13 144:skull=0-5,8-13 145:anvil=0-11 146:trapped_chest=2-5
147:light_weighted_pressure_plate=0-15 148:heavy_weighted_pressure_plate=0-15 149:unpowered_comparator=0-15
150:powered_comparator=0-15 151:daylight_detector=0-15 152:redstone_block=0 153:quartz_ore=0 154:hopper=0,2-5,8,10-13
155:quartz_block=0-4 156:quartz_stairs=0-7 157:activator_rail=0-5,8-13 158:dropper=0-5,8-13
159:stained_hardened_clay=0-15 160:stained_glass_pane=0-15 161:leaves2=0-1,4-5,8-9,12-13 162:log2=0-1,4-5,8-9,12-13
163:acacia_stairs=0-7 164:dark_oak_stairs=0-7 165:slime=0 166:barrier=0 167:iron_trapdoor=0-15 168:prismarine=0-2
169:sea_lantern=0 170:hay_block=0,4,8 171:carpet=0-15 172:hardened_clay=0 173:coal_block=0 174:packed_ice=0
175:double_plant=0-5,8-11 176:standing_banner=0-15 177:wall_banner=2-5 178:daylight_detector_inverted=0-15
179:red_sandstone=0-2 180:red_sandstone_stairs=0-7 181:double_stone_slab2=0,8 182:stone_slab2=0,8
183:spruce_fence_gate=0-15 184:birch_fence_gate=0-15 185:jungle_fence_gate=0-15 186:dark_oak_fence_gate=0-15
187:acacia_fence_gate=0-15 188:spruce_fence=0 189:birch_fence=0 190:jungle_fence=0 191:dark_oak_fence=0
192:acacia_fence=0 193:spruce_door=0-11 194:birch_door=0-11 195:jungle_door=0-11 196:acacia_door=0-11
197:dark_oak_door=0-11 198:end_rod=0-5 199:chorus_plant=0 200:chorus_flower=0-5 201:purpur_block=0
202:purpur_pillar=0,4,8 203:purpur_stairs=0-7 204:purpur_double_slab=0 205:purpur_slab=0,8 206:end_bricks=0
207:beetroots=0-3 208:grass_path=0 209:end_gateway=0 210:repeating_command_block=0-5,8-13
211:chain_command_block=0-5,8-13 212:frosted_ice=0-3 213:magma=0 214:nether_wart_block=0 215:red_nether_brick=0
216:bone_block=0,4,8 217:structure_void=0 218:observer=0-5,8-13 219:white_shulker_box=0-5 220:orange_shulker_box=0-5
221:magenta_shulker_box=0-5 222:light_blue_shulker_box=0-5 223:yellow_shulker_box=0-5 224:lime_shulker_box=0-5
225:pink_shulker_box=0-5 226:gray_shulker_box=0-5 227:silver_shulker_box=0-5 228:cyan_shulker_box=0-5
229:purple_shulker_box=0-5 230:blue_shulker_box=0-5 231:brown_shulker_box=0-5 232:green_shulker_box=0-5
233:red_shulker_box=0-5 234:black_shulker_box=0-5 235:white_glazed_terracotta=0-3 236:orange_glazed_terracotta=0-3
237:magenta_glazed_terracotta=0-3 238:light_blue_glazed_terracotta=0-3 239:yellow_glazed_terracotta=0-3
240:lime_glazed_terracotta=0-3 241:pink_glazed_terracotta=0-3 242:gray_glazed_terracotta=0-3
243:silver_glazed_terracotta=0-3 244:cyan_glazed_terracotta=0-3 245:purple_glazed_terracotta=0-3
246:blue_glazed_terracotta=0-3 247:brown_glazed_terracotta=0-3 248:green_glazed_terracotta=0-3
249:red_glazed_terracotta=0-3 250:black_glazed_terracotta=0-3 251:concrete=0-15 252:concrete_powder=0-15
255:structure_block=0-3
"""
# `name=damage ranges` of the items whose damage value picks the flattened item (ItemStackTheFlatteningFix).
ITEM_DAMAGES = """
stone=0-6 dirt=0-2 leaves=0-3 leaves2=0-1 log=0-3 log2=0-1 sapling=0-5 planks=0-5 sand=0-1 quartz_block=0-2 anvil=0-2
wool=0-15 carpet=0-15 hardened_clay=0 stained_hardened_clay=0-15 silver_glazed_terracotta=0 stained_glass=0-15
stained_glass_pane=0-15 prismarine=0-2 concrete=0-15 concrete_powder=0-15 cobblestone_wall=0-1 sandstone=0-2
red_sandstone=0-2 stonebrick=0-3 monster_egg=0-5 yellow_flower=0 red_flower=0-8 double_plant=0-5 deadbush=0
tallgrass=0-2 sponge=0-1 purpur_slab=0 stone_slab=0-7 stone_slab2=0 wooden_slab=0-5 coal=0-1 fish=0-3 cooked_fish=0-1
skull=0-5 golden_apple=0-1 fireworks=0 firework_charge=0 dye=0-15 silver_shulker_box=0 fence=0 fence_gate=0
wooden_door=0 boat=0 lit_pumpkin=0 pumpkin=0 trapdoor=0 nether_brick=0 red_nether_brick=0 netherbrick=0
wooden_button=0 wooden_pressure_plate=0 noteblock=0 bed=0-15 banner=0-15 grass=0 brick_block=0 end_bricks=0
golden_rail=0 magma=0 quartz_ore=0 reeds=0 slime=0 stone_stairs=0 waterlily=0 web=0 snow=0 snow_layer=0 record_11=0
record_13=0 record_blocks=0 record_cat=0 record_chirp=0 record_far=0 record_mall=0 record_mellohi=0 record_stal=0
record_strad=0 record_wait=0 record_ward=0
"""
# Items a 1.12.2 server accepts (numeric ids 0..422 plus later ones).
ITEM_NAMES = """
stone grass dirt cobblestone planks sapling bedrock sand gravel gold_ore iron_ore coal_ore log leaves sponge glass
lapis_ore lapis_block dispenser sandstone noteblock golden_rail detector_rail sticky_piston web tallgrass deadbush
piston wool yellow_flower red_flower brown_mushroom red_mushroom gold_block iron_block stone_slab brick_block tnt
bookshelf mossy_cobblestone obsidian torch mob_spawner oak_stairs chest diamond_ore diamond_block crafting_table
farmland furnace ladder rail stone_stairs lever stone_pressure_plate wooden_pressure_plate redstone_ore redstone_torch
stone_button snow_layer ice snow cactus clay jukebox fence pumpkin netherrack soul_sand glowstone lit_pumpkin
stained_glass trapdoor monster_egg stonebrick brown_mushroom_block red_mushroom_block iron_bars glass_pane melon_block
vine fence_gate brick_stairs stone_brick_stairs mycelium waterlily nether_brick nether_brick_fence nether_brick_stairs
enchanting_table end_portal_frame end_stone dragon_egg redstone_lamp wooden_slab sandstone_stairs emerald_ore
ender_chest tripwire_hook emerald_block spruce_stairs birch_stairs jungle_stairs command_block beacon cobblestone_wall
wooden_button anvil trapped_chest light_weighted_pressure_plate heavy_weighted_pressure_plate daylight_detector
redstone_block quartz_ore hopper quartz_block quartz_stairs activator_rail dropper stained_hardened_clay
stained_glass_pane leaves2 log2 acacia_stairs dark_oak_stairs hay_block carpet hardened_clay coal_block packed_ice
double_plant iron_shovel iron_pickaxe iron_axe flint_and_steel apple bow arrow coal diamond iron_ingot gold_ingot
iron_sword wooden_sword wooden_shovel wooden_pickaxe wooden_axe stone_sword stone_shovel stone_pickaxe stone_axe
diamond_sword diamond_shovel diamond_pickaxe diamond_axe stick bowl mushroom_stew golden_sword golden_shovel
golden_pickaxe golden_axe string feather gunpowder wooden_hoe stone_hoe iron_hoe diamond_hoe golden_hoe wheat_seeds
wheat bread leather_helmet leather_chestplate leather_leggings leather_boots chainmail_helmet chainmail_chestplate
chainmail_leggings chainmail_boots iron_helmet iron_chestplate iron_leggings iron_boots diamond_helmet
diamond_chestplate diamond_leggings diamond_boots golden_helmet golden_chestplate golden_leggings golden_boots flint
porkchop cooked_porkchop painting golden_apple sign wooden_door bucket water_bucket lava_bucket minecart saddle
iron_door redstone snowball boat leather milk_bucket brick clay_ball reeds paper book slime_ball chest_minecart
furnace_minecart egg compass fishing_rod clock glowstone_dust fish dye bone sugar cake bed repeater cookie filled_map
shears melon pumpkin_seeds melon_seeds beef cooked_beef chicken cooked_chicken rotten_flesh ender_pearl blaze_rod
ghast_tear gold_nugget nether_wart potion glass_bottle spider_eye fermented_spider_eye blaze_powder magma_cream
brewing_stand cauldron ender_eye speckled_melon spawn_egg experience_bottle fire_charge writable_book written_book
emerald item_frame flower_pot carrot potato baked_potato poisonous_potato map golden_carrot skull carrot_on_a_stick
nether_star pumpkin_pie fireworks firework_charge enchanted_book comparator netherbrick quartz tnt_minecart
hopper_minecart iron_horse_armor golden_horse_armor diamond_horse_armor lead name_tag command_block_minecart record_13
record_cat record_blocks record_chirp record_far record_mall record_mellohi record_stal record_strad record_ward
record_11 record_wait cooked_fish shield elytra totem_of_undying spectral_arrow tipped_arrow splash_potion
lingering_potion beetroot beetroot_seeds beetroot_soup dragon_breath chorus_fruit chorus_flower end_crystal
shulker_shell iron_nugget knowledge_book spruce_boat birch_boat jungle_boat acacia_boat dark_oak_boat mutton
cooked_mutton rabbit cooked_rabbit rabbit_stew rabbit_foot rabbit_hide prismarine_shard prismarine_crystals
purpur_block end_rod structure_block structure_void barrier grass_path observer white_shulker_box silver_shulker_box
white_glazed_terracotta concrete concrete_powder bone_block magma nether_wart_block red_nether_brick
repeating_command_block chain_command_block sea_lantern prismarine red_sandstone red_sandstone_stairs purpur_stairs
purpur_slab purpur_pillar end_bricks spruce_door birch_door jungle_door acacia_door dark_oak_door spruce_fence_gate
spruce_fence stone_slab2 banner iron_trapdoor
"""
def parse_ranges(text):
out = []
for part in text.split(','):
a, _, b = part.partition('-')
out.extend(range(int(a), int(b or a) + 1))
return out
BLOCK_STATES = {} # name -> list of data values
BLOCK_IDS = {} # name -> numeric id
for _entry in BLOCKS.split():
_id, _, _rest = _entry.partition(':')
_name, _, _data = _rest.partition('=')
BLOCK_IDS[_name] = int(_id)
BLOCK_STATES[_name] = parse_ranges(_data)
ITEM_VARIANTS = {}
for _entry in ITEM_DAMAGES.split():
_name, _, _data = _entry.partition('=')
ITEM_VARIANTS[_name] = parse_ranges(_data)
ITEMS = ITEM_NAMES.split()
COLORS = ['white', 'orange', 'magenta', 'light_blue', 'yellow', 'lime', 'pink', 'gray', 'silver', 'cyan', 'purple',
'blue', 'brown', 'green', 'red', 'black']
WOODS = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak']
# ------------------------------------------------------------------ NBT text helpers
def q(s):
"""A quoted NBT string."""
return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
def js(obj):
"""A JSON text as an NBT string (1.12 stores text components as JSON strings)."""
if isinstance(obj, str) and ERA < 12:
obj = {'text': obj} # before 1.12 a JSON string primitive makes the command fail
return q(json.dumps(obj, separators=(',', ':')))
def full(name):
return name if ':' in name else 'minecraft:' + name
class Stack(str):
"""An item compound that remembers its parts (for `replaceitem` / `give`)."""
def __new__(cls, text, name, count, damage, tag, numeric):
obj = str.__new__(cls, text)
obj.name, obj.count, obj.damage, obj.tag, obj.numeric = name, count, damage, tag, numeric
return obj
def command_args(self):
return f'{full(self.name)} {self.count} {self.damage}' + (f' {self.tag}' if self.tag else '')
def item(name, count=1, damage=0, tag=None, slot=None, numeric=None):
"""An item stack compound. `numeric`: write the 1.7 style numeric `id:Ns` instead of the name."""
if VALID_ITEMS is not None and numeric is None and name not in VALID_ITEMS:
name, tag = 'stick', None # an item this version lacks
parts = []
if slot is not None:
parts.append(f'Slot:{slot}b')
parts.append(f'id:{numeric}s' if numeric is not None else f'id:{q(full(name))}')
parts.append(f'Count:{count}b')
parts.append(f'Damage:{damage}s')
if tag:
parts.append(f'tag:{tag}')
return Stack('{' + ','.join(parts) + '}', name, count, damage, tag, numeric)
def slotted(stacks, first=0):
"""Put `Slot` into the stack compounds (strings starting with `{`)."""
return '[' + ','.join('{Slot:%db,%s' % (first + i, s[1:]) for i, s in enumerate(stacks)) + ']'
def ench(*pairs):
"""`ench:[{id:16s,lvl:3s}]`: 1.12 enchantments are numeric ids."""
return 'ench:[' + ','.join('{id:%ds,lvl:%ds}' % p for p in pairs) + ']'
def stored_ench(*pairs):
return 'StoredEnchantments:[' + ','.join('{id:%ds,lvl:%ds}' % p for p in pairs) + ']'
def display(name=None, lore=None, color=None):
parts = []
if name is not None:
parts.append(f'Name:{q(name)}')
if lore:
parts.append('Lore:[' + ','.join(q(l) for l in lore) + ']')
if color is not None:
parts.append(f'color:{color}')
return 'display:{' + ','.join(parts) + '}'
# ------------------------------------------------------------------ running commands in batches
FAIL = re.compile(r"(couldn't|cannot|can't|unknown|usage:|invalid|no such|not found|outside of the world|"
r"exception|expected|failed|unable|could not|does not exist|there is no|not a valid|"
r"incorrect|malformed|error|no entity|must be|too (big|large|many|long)|not allowed|"
r"not an? |no (item|block|player|entities))", re.I)
FINE = re.compile(r"(already have it|Can't keep up|No blocks filled|logged in with entity id|lost connection|left the game|joined the game)")
def clean(line):
return re.sub(r'^\[[0-9:]+\] \[[^\]]*\]: ', '', line)
def batch(server, commands, problems, chunk=120, quiet=False):
"""Run the commands one after the other (many per tick), return their output lines. Failures go to `problems`."""
outputs = []
for start in range(0, len(commands), chunk):
part = commands[start:start + chunk]
marks = []
text = []
for c in part:
server.counter += 1
mark = f'@@{server.counter}@@'
marks.append(mark)
text.append(c)
text.append('say ' + mark)
server.process.stdin.write('\n'.join(text) + '\n')
server.process.stdin.flush()
i = 0
current = []
deadline = time.time() + 600
while i < len(part):
try:
line = clean(server.queue.get(timeout=5))
except queue.Empty:
if server.process.poll() is not None or time.time() > deadline:
raise TimeoutError(f'server did not answer {part[i][:100]!r}')
continue
if marks[i] in line:
outputs.append(current)
bad = [l for l in current if FAIL.search(l) and not FINE.search(l)]
if bad and not quiet:
problems.append((part[i][:200], bad[:2]))
current = []
i += 1
else:
current.append(line)
return outputs
# ------------------------------------------------------------------ layout
class Layers:
"""Platforms are created on first use; rows of them are handed out to `Packer`s."""
def __init__(self, scene):
self.scene = scene
self.next = 0
self.made = set()
self.y = None
self.z = 0
self.n = 0
def take(self):
"""A whole new platform."""
if self.next >= len(LAYERS):
raise RuntimeError('out of layers')
y = LAYERS[self.next]
self.next += 1
self.platform(y)
return y
def fresh(self):
"""The next row goes on a new platform (so that the platform is used by one group of things only)."""
self.y = None
def row(self, zstep):
"""The next free row (y, z, parity) of the current platform."""
if self.y is None or self.z + zstep > BZ + SIZE - 2:
self.y = self.take()
self.z = BZ + 1
z = self.z
self.z += zstep + (1 if self.n % 2 == 0 else 0)
self.n += 1
return self.y, z, (self.n % 2)
def platform(self, y):
if y in self.made:
return
self.made.add(y)
s = self.scene
if y == LAYERS[0]: # clear the natural terrain around the first platform
for x in range(BX, BX + SIZE, 32):
for z in range(BZ, BZ + SIZE, 32):
s.cmd(f'fill {x} {y} {z} {x + 31} {y + 11} {z + 31} minecraft:air 0')
s.cmd(f'fill {BX} {y - 2} {BZ} {BX + SIZE - 1} {y - 1} {BZ + SIZE - 1} minecraft:stone 0')
class Packer:
"""Hands out cells in rows of the platforms: `cell()` -> (x, y, z), cells `stride` apart.
Rows alternate their start so that both chunk edges (x % 16 == 0 and 15) are used."""
def __init__(self, layers, stride=2, zstep=3):
self.layers, self.stride, self.zstep = layers, stride, zstep
self.y = self.x = self.z = None
def newrow(self):
self.y, self.z, parity = self.layers.row(self.zstep)
self.x = BX + parity
def cell(self, span=1):
"""One cell with room for `span` cells in x."""
if self.x is None or self.x + self.stride * span > BX + SIZE - 1:
self.newrow()
x = self.x
self.x += self.stride
return x, self.y, self.z
def gap(self, n=1):
if self.x is not None:
self.x += self.stride * n
def take(self, n, span=1):
return [self.cell(span) for _ in range(n)]
class Scene:
def __init__(self):
self.cmds = [] # the first phase
self.late = [] # the last tick
self.expect = {} # (x, y, z) -> (block, data), what the scene placed
self.tes = {} # (x, y, z) -> block entity id expected
self.layers = Layers(self)
def cmd(self, c, late=False):
(self.late if late else self.cmds).append(c)
def block(self, x, y, z, name, data=0, tag=None, late=False, expect=True):
if not block_ok(name):
return
if name in TWO_STEP and data:
self.cmd(f'setblock {x} {y} {z} {full(name)} 0 replace', late)
c = f'setblock {x} {y} {z} {full(name)} {data} replace' + (f' {tag}' if tag else '')
self.cmd(c, late)
if expect:
self.expect[(x, y, z)] = (name, data)
def fill(self, x1, y1, z1, x2, y2, z2, name, data=0, late=False):
self.cmd(f'fill {x1} {y1} {z1} {x2} {y2} {z2} {full(name)} {data}', late)
def summon(self, kind, x, y, z, nbt='', late=False):
self.cmd(f'summon {kind} {x} {y} {z} {{{nbt}}}', late)
# ------------------------------------------------------------------ items
POTIONS = ['water', 'mundane', 'thick', 'awkward', 'night_vision', 'long_night_vision', 'invisibility', 'long_invisibility',
'leaping', 'strong_leaping', 'long_leaping', 'fire_resistance', 'long_fire_resistance', 'swiftness',
'strong_swiftness', 'long_swiftness', 'slowness', 'long_slowness', 'water_breathing', 'long_water_breathing',
'healing', 'strong_healing', 'harming', 'strong_harming', 'poison', 'strong_poison', 'long_poison',
'regeneration', 'strong_regeneration', 'long_regeneration', 'strength', 'strong_strength', 'long_strength',
'weakness', 'long_weakness', 'luck']
ENCHANT_IDS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 16, 17, 18, 19, 20, 21, 22, 32, 33, 34, 35, 48, 49, 50, 51, 61, 62, 70, 71]
# 1.12 entity ids that a spawn egg or a spawner can carry
MOBS = ['bat', 'blaze', 'cave_spider', 'chicken', 'cow', 'creeper', 'donkey', 'elder_guardian', 'enderman', 'endermite',
'evocation_illager', 'ghast', 'guardian', 'horse', 'husk', 'llama', 'magma_cube', 'mooshroom', 'mule', 'ocelot',
'parrot', 'pig', 'polar_bear', 'rabbit', 'sheep', 'shulker', 'silverfish', 'skeleton', 'skeleton_horse', 'slime',
'spider', 'squid', 'stray', 'vex', 'villager', 'vindication_illager', 'witch', 'wither_skeleton', 'wolf',
'zombie', 'zombie_horse', 'zombie_pigman', 'zombie_villager', 'villager_golem', 'snowman', 'illusion_illager']
OLD_NAMES = {'cave_spider': 'CaveSpider', 'elder_guardian': 'ElderGuardian', 'evocation_illager': 'EvocationIllager',
'magma_cube': 'LavaSlime', 'mooshroom': 'MushroomCow', 'ocelot': 'Ozelot', 'polar_bear': 'PolarBear',
'skeleton_horse': 'SkeletonHorse', 'vindication_illager': 'VindicationIllager',
'wither_skeleton': 'WitherSkeleton', 'zombie_horse': 'ZombieHorse', 'zombie_pigman': 'PigZombie',
'zombie_villager': 'ZombieVillager', 'villager_golem': 'VillagerGolem', 'snowman': 'SnowMan',
'illusion_illager': 'IllusionIllager'}
# Entity ids by era. 1.11 on: `minecraft:` snake_case names (as in `MOBS`); before: CamelCase ids and merged types.
CAMEL = {
'zombie': 'Zombie', 'skeleton': 'Skeleton', 'creeper': 'Creeper', 'spider': 'Spider', 'cave_spider': 'CaveSpider',
'silverfish': 'Silverfish', 'endermite': 'Endermite', 'blaze': 'Blaze', 'ghast': 'Ghast', 'witch': 'Witch', 'giant': 'Giant',
'guardian': 'Guardian', 'shulker': 'Shulker', 'zombie_pigman': 'PigZombie', 'slime': 'Slime', 'magma_cube': 'LavaSlime',
'enderman': 'Enderman', 'bat': 'Bat', 'squid': 'Squid', 'pig': 'Pig', 'cow': 'Cow', 'chicken': 'Chicken',
'mooshroom': 'MushroomCow', 'sheep': 'Sheep', 'wolf': 'Wolf', 'ocelot': 'Ozelot', 'horse': 'EntityHorse',
'rabbit': 'Rabbit', 'polar_bear': 'PolarBear', 'villager': 'Villager', 'villager_golem': 'VillagerGolem',
'snowman': 'SnowMan', 'armor_stand': 'ArmorStand', 'painting': 'Painting', 'item_frame': 'ItemFrame',
'leash_knot': 'LeashKnot', 'item': 'Item', 'xp_orb': 'XPOrb', 'minecart': 'MinecartRideable',
'chest_minecart': 'MinecartChest', 'furnace_minecart': 'MinecartFurnace', 'hopper_minecart': 'MinecartHopper',
'tnt_minecart': 'MinecartTNT', 'spawner_minecart': 'MinecartSpawner', 'commandblock_minecart': 'MinecartCommandBlock',
'boat': 'Boat', 'tnt': 'PrimedTnt', 'falling_block': 'FallingSand', 'arrow': 'Arrow', 'spectral_arrow': 'SpectralArrow',
'potion': 'ThrownPotion', 'egg': 'ThrownEgg', 'snowball': 'Snowball', 'ender_pearl': 'ThrownEnderpearl',
'xp_bottle': 'ThrownExpBottle', 'eye_of_ender_signal': 'EyeOfEnderSignal', 'fireball': 'Fireball',
'small_fireball': 'SmallFireball', 'dragon_fireball': 'DragonFireball', 'wither_skull': 'WitherSkull',
'shulker_bullet': 'ShulkerBullet', 'ender_crystal': 'EnderCrystal', 'fireworks_rocket': 'FireworksRocketEntity',
'area_effect_cloud': 'AreaEffectCloud',
}
# types that were one entity with a type field before 1.11: kind -> (CamelCase id, extra NBT)
MERGED = {
'wither_skeleton': ('Skeleton', 'SkeletonType:1b'), 'stray': ('Skeleton', 'SkeletonType:2b'), 'husk': ('Zombie', 'ZombieType:6'),
'zombie_villager': ('Zombie', 'ZombieType:2'), 'donkey': ('EntityHorse', 'Type:1'), 'mule': ('EntityHorse', 'Type:2'),
'zombie_horse': ('EntityHorse', 'Type:3'), 'skeleton_horse': ('EntityHorse', 'Type:4'), 'elder_guardian': ('Guardian', 'Elder:1b'),
}
ABSENT = { # kinds a version lacks
# (thrown potions: vanilla's fix of key 705 crashes on them, so none before 1.11; 1.8 zombie villagers get a random profession)
8: {'potion', 'zombie_villager', 'shulker', 'shulker_bullet', 'area_effect_cloud', 'spectral_arrow', 'dragon_fireball', 'polar_bear', 'husk', 'stray',
'vex', 'evocation_illager', 'vindication_illager', 'illusion_illager', 'llama', 'llama_spit', 'evocation_fangs', 'parrot'},
9: {'potion', 'polar_bear', 'husk', 'stray', 'vex', 'evocation_illager', 'vindication_illager', 'illusion_illager', 'llama',
'llama_spit', 'evocation_fangs', 'parrot'},
10: {'potion', 'vex', 'evocation_illager', 'vindication_illager', 'illusion_illager', 'llama', 'llama_spit', 'evocation_fangs', 'parrot'},
11: {'parrot', 'illusion_illager'},
12: set(),
}
def entity_form(kind, profession=None):
"""(summon id, extra NBT) of an entity kind in this era, None if the version lacks it."""
if kind in ABSENT[ERA]:
return None
if ERA >= 11:
return kind, ''
if kind in MERGED:
sid, extra = MERGED[kind]
if kind == 'zombie_villager':
extra = 'IsVillager:1b' if ERA == 8 else 'ZombieType:%d' % (1 + (profession or 0) % 5)
return sid, extra
return CAMEL[kind], ''
def eid(kind):
"""The quoted `id` for a kind inside NBT."""
form = entity_form(kind)
sid = form[0] if form else kind
return q(sid if ERA <= 10 else 'minecraft:' + sid)
BANNER_PATTERNS = ['b', 'bs', 'ls', 'rs', 'ts', 'ms', 'drs', 'dls', 'ss', 'cr', 'sc', 'bt', 'tt', 'bts', 'tts', 'ld', 'rd',
'lud', 'rud', 'mc', 'mr', 'vh', 'hh', 'vhr', 'hhb', 'bo', 'cbo', 'br', 'tl', 'bl', 'tr', 'gra', 'gru',
'bri', 'sku', 'cre', 'flo', 'moj', 'cs']
RECORDS = ['record_13', 'record_cat', 'record_blocks', 'record_chirp', 'record_far', 'record_mall', 'record_mellohi',
'record_stal', 'record_strad', 'record_ward', 'record_11', 'record_wait']
DAMAGEABLE = ['bow', 'carrot_on_a_stick', 'chainmail_boots', 'chainmail_chestplate', 'chainmail_helmet', 'chainmail_leggings',
'diamond_axe', 'diamond_boots', 'diamond_chestplate', 'diamond_helmet', 'diamond_hoe', 'diamond_leggings',
'diamond_pickaxe', 'diamond_shovel', 'diamond_sword', 'elytra', 'fishing_rod', 'flint_and_steel', 'golden_axe',
'golden_boots', 'golden_chestplate', 'golden_helmet', 'golden_hoe', 'golden_leggings', 'golden_pickaxe',
'golden_shovel', 'golden_sword', 'iron_axe', 'iron_boots', 'iron_chestplate', 'iron_helmet', 'iron_hoe',
'iron_leggings', 'iron_pickaxe', 'iron_shovel', 'iron_sword', 'leather_boots', 'leather_chestplate',
'leather_helmet', 'leather_leggings', 'shears', 'shield', 'stone_axe', 'stone_hoe', 'stone_pickaxe',
'stone_shovel', 'stone_sword', 'wooden_axe', 'wooden_hoe', 'wooden_pickaxe', 'wooden_shovel', 'wooden_sword']
def sweep_stacks():
"""Every kind of item the flattening cares about, one stack each (no slot yet)."""
out = []
for n in ITEMS:
for d in ITEM_VARIANTS.get(n, [0]):
out.append(item(n, damage=d))
# items whose damage is wear
for i, n in enumerate(DAMAGEABLE):
out.append(item(n, damage=(i * 7) % 60 + 1, tag='{Damage:%d,RepairCost:%d}' % ((i * 7) % 60 + 1, i % 5)))
# damage values the flattening has no entry for, and non-damageable items with damage
for n, d in [('stone', 7), ('stone', 15), ('wool', 16), ('wool', 100), ('log', 4), ('log', 15), ('planks', 6), ('dirt', 3),
('sand', 2), ('apple', 3), ('stick', 9), ('dye', 16), ('leaves', 8), ('leaves2', 5), ('stonebrick', 7),
('quartz_block', 5), ('prismarine', 3), ('anvil', 3), ('coal', 2), ('double_plant', 6), ('red_flower', 9),
('tallgrass', 3), ('sapling', 6), ('cobblestone_wall', 2), ('monster_egg', 6), ('sponge', 2)]:
out.append(item(n, damage=d))
out.append(item('stone', damage=-1))
# raw numeric ids (as written by a 1.7 server) are kept inside item tags
for nid, d in [(1, 0), (35, 5), (5, 3), (17, 2), (351, 4), (397, 3), (383, 50), (373, 8261), (322, 1), (349, 3), (263, 1),
(160, 7), (159, 14), (251, 3), (425, 0), (397, 5), (298, 0), (276, 12)]:
out.append(item('air', damage=d, numeric=nid))
# potions in all four shapes
if ERA == 8: # before 1.9 a potion is its damage value: effect, level and extended / splash bits
for d in list(range(0, 64)) + [8193, 8194, 8195, 8196, 8197, 8198, 8200, 8201, 8202, 8204, 8205, 8206, 8225, 8226, 8228, 8229, 8233,
8234, 8236, 8257, 8258, 8259, 8260, 8261, 8262, 8264, 8265, 8266, 8270, 16385, 16386, 16387, 16388,
16389, 16392, 16393, 16394, 16396, 16418, 16420, 16421, 16449, 16450, 16451, 16452, 16453, 16457, 16458]:
out.append(item('potion', damage=d))
for kind in ('potion', 'splash_potion', 'lingering_potion', 'tipped_arrow') if ERA >= 9 else ():
for p in POTIONS:
out.append(item(kind, count=1 if kind != 'tipped_arrow' else 7, tag='{Potion:%s}' % q('minecraft:' + p)))
out.append(item(kind, tag='{CustomPotionEffects:[{Id:1b,Amplifier:1b,Duration:600,Ambient:0b,ShowParticles:1b},'
'{Id:10b,Amplifier:0b,Duration:200}],CustomPotionColor:16711680,Potion:"minecraft:awkward"}'))
out.append(item('potion', damage=8261))
out.append(item('potion', damage=16453))
out.append(item('potion', damage=32764))
# enchantments, numeric ids
for e in ENCHANT_IDS:
out.append(item('enchanted_book', tag='{%s}' % stored_ench((e, 1 + e % 5), (34, 3))))
out.append(item(DAMAGEABLE[e % len(DAMAGEABLE)], damage=e, tag='{%s,Damage:%d}' % (ench((e, 1 + e % 4), ((e + 7) % 35, 2)), e)))
out.append(item('enchanted_book', tag='{StoredEnchantments:[{id:70s,lvl:1s},{id:71s,lvl:1s},{id:99s,lvl:1s}]}'))
out.append(item('diamond_sword', tag='{ench:[{id:16s,lvl:32767s},{id:-1s,lvl:0s}],Unbreakable:1b,HideFlags:63}'))
# spawn eggs, old style: the entity id in EntityTag
for i, m in enumerate(MOBS):
if entity_form(m) is None:
continue
out.append(item('spawn_egg', tag='{EntityTag:{id:%s}}' % eid(m)))
if m in OLD_NAMES or i % 3 == 0:
out.append(item('spawn_egg', tag='{EntityTag:{id:%s,CustomName:"Egg %d"}}' % (q(OLD_NAMES.get(m, m.title())), i)))
out.append(item('spawn_egg', damage=50, tag='{EntityTag:{id:"minecraft:creeper",powered:1b}}'))
out.append(item('spawn_egg'))
for i in range(0, 130, 9):
out.append(item('spawn_egg', damage=i))
# names, lore, colors
out.append(item('diamond_sword', tag='{%s,Damage:3}' % display('Blade', ['first line', 'second line'])))
out.append(item('stick', tag='{%s}' % display('{"text":"json looking"}', ['{"text":"lore"}'])))
out.append(item('paper', tag='{%s}' % display(r'§cColoured §lname', ['§7grey lore'])))
out.append(item('stone', tag='{%s,ench:[],Unbreakable:0b}' % display('')))
for i, c in enumerate([0, 255, 16711680, 65280, 8388608, 16777215, 10511680]):
out.append(item(['leather_helmet', 'leather_chestplate', 'leather_leggings', 'leather_boots'][i % 4], damage=i,
tag='{%s}' % display(color=c)))
# attribute modifiers
out.append(item('iron_sword', tag='{AttributeModifiers:[{AttributeName:"generic.attackDamage",Name:"x",Amount:4.5d,Operation:0,'
'UUIDMost:1L,UUIDLeast:2L,Slot:"mainhand"},{AttributeName:"generic.movementSpeed",Name:"y",'
'Amount:0.1d,Operation:1,UUIDMost:3L,UUIDLeast:4L}]}'))
# CanDestroy / CanPlaceOn
out.append(item('diamond_pickaxe', tag='{CanDestroy:["minecraft:stone","dirt","minecraft:log","wool"]}'))
out.append(item('stone', count=3, tag='{CanPlaceOn:["minecraft:grass","sand","minecraft:planks"]}'))
out.append(item('diamond_pickaxe', tag='{CanDestroy:[1,2,3,35,17]}'))
out.append(item('stone', tag='{CanPlaceOn:[2,12,5],CanDestroy:["minecraft:flowing_water"]}'))
# banners and shields
for i, c in enumerate(COLORS):
pats = ','.join('{Pattern:%s,Color:%d}' % (q(BANNER_PATTERNS[(i * 3 + k) % len(BANNER_PATTERNS)]), (i + 5 * k) % 16)
for k in range(1 + i % 5))
out.append(item('banner', damage=i, tag='{BlockEntityTag:{Base:%d,Patterns:[%s]}}' % (i, pats)))
if i % 2 == 0:
out.append(item('shield', damage=i, tag='{BlockEntityTag:{Base:%d,Patterns:[%s]}}' % (15 - i, pats)))
out.append(item('banner', damage=4, tag='{BlockEntityTag:{Patterns:[{Pattern:"mr",Color:9}]},%s}' % display('Ominous Banner')))
# skulls
for t in range(6):
out.append(item('skull', damage=t))
out.append(item('skull', damage=3, tag='{SkullOwner:"Notch"}'))
out.append(item('skull', damage=3, tag='{SkullOwner:{Id:"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6",Name:"Dinnerbone",'
'Properties:{textures:[{Value:"eyJ0ZXh0dXJlcyI6e319",Signature:"sig"}]}}}'))
# books
out.append(item('written_book', tag='{title:"Title",author:"Me",generation:1,resolved:1b,pages:[%s,%s,"plain page"]}'
% (js({'text': 'page one', 'bold': True}), js({'text': '', 'extra': [{'text': 'two', 'clickEvent': {
'action': 'change_page', 'value': '1'}}, {'text': ' hover', 'hoverEvent': {
'action': 'show_text', 'value': {'text': 'hi'}}}]}))))
out.append(item('written_book', tag='{title:"Old",author:"Ancient",generation:0,pages:["old \\"string\\" page"]}'))
out.append(item('writable_book', tag='{pages:["draft one","draft two"]}'))
# fireworks
out.append(item('fireworks', count=3, tag='{Fireworks:{Flight:2b,Explosions:[{Type:1b,Colors:[I;255,16711680],FadeColors:[I;65280],'
'Trail:1b,Flicker:0b},{Type:4b,Colors:[I;1],Trail:0b}]}}'))
out.append(item('firework_charge', tag='{Explosion:{Type:3b,Colors:[I;65280],FadeColors:[I;1,2,3],Flicker:1b}}'))
# nested containers in item tags
inner = [item('stone', count=9, slot=0), item('golden_apple', damage=1, slot=3, tag='{%s}' % display('Gold')),
item('dye', damage=4, slot=5), item('wool', damage=14, slot=26), item('air', numeric=35, damage=2, slot=1)]
for i, c in enumerate(COLORS):
out.append(item(c + '_shulker_box' if c != 'silver' else 'silver_shulker_box', tag='{BlockEntityTag:{Items:[%s],id:"minecraft:shulker_box"'
',CustomName:%s,Lock:"key%d"}}' % (','.join(inner[:1 + i % 5]), q('Box ' + c), i)))
out.append(item('chest', tag='{BlockEntityTag:{Items:[%s],id:"minecraft:chest"}}' % ','.join(inner)))
out.append(item('chest', tag='{BlockEntityTag:{LootTable:"minecraft:chests/simple_dungeon",LootTableSeed:5L}}'))
out.append(item('trapped_chest', tag='{BlockEntityTag:{Items:[%s]}}' % item('wool', damage=3, slot=0)))
out.append(item('furnace', tag='{BlockEntityTag:{Items:[%s],BurnTime:100s,CookTime:50s,CookTimeTotal:200s}}' % item('iron_ore', slot=0)))
out.append(item('mob_spawner', tag='{BlockEntityTag:{SpawnData:{id:"Zombie"},SpawnPotentials:[{Entity:{id:"Skeleton"},Weight:2}]}}'))
out.append(item('beacon', tag='{BlockEntityTag:{Levels:3,Primary:1,Secondary:10}}'))
out.append(item('command_block', tag='{BlockEntityTag:{Command:"say item",TrackOutput:0b}}'))
out.append(item('noteblock', tag='{BlockEntityTag:{note:13b}}'))
out.append(item('standing_banner', damage=3))
out.append(item('flower_pot', count=2))
out.append(item('monster_egg', damage=5))
out.append(item('stained_glass_pane', damage=11, count=16))
out.append(item('filled_map', damage=0, tag='{display:{MapColor:255}}'))
out.append(item('filled_map', damage=1))
out.append(item('filled_map', damage=2, tag='{map_is_scaling:1b}'))
out.append(item('map', count=2))
out.append(item('name_tag', tag='{%s}' % display('Named')))
out.append(item('minecart', tag='{EntityTag:{id:"minecraft:minecart"}}'))
out.append(item('armor_stand', tag='{EntityTag:{id:"minecraft:armor_stand",ShowArms:1b,Small:1b}}'))
out.append(item('painting', tag='{EntityTag:{Motive:"Pigscene"}}'))
out.append(item('item_frame', tag='{EntityTag:{Item:%s}}' % item('wool', damage=5)))
out.append(item('knowledge_book', tag='{Recipes:["minecraft:stick","minecraft:wooden_slab","minecraft:stained_hardened_clay"]}'))
out.append(item('totem_of_undying'))
out.append(item('golden_apple', damage=1, count=3))
out.append(item('banner', damage=1, tag='{display:{Name:"Plain"}}'))
for r in RECORDS:
out.append(item(r))
return out
class Stacks:
"""A cursor over the sweep: containers take the next stacks, and wrap around at the end."""
def __init__(self):
self.stacks = sweep_stacks()
if VALID_ITEMS is not None: # the items this version lacks became sticks: keep one
self.stacks = list(dict.fromkeys(self.stacks))
self.i = 0
def take(self, n):
out = [self.stacks[(self.i + k) % len(self.stacks)] for k in range(n)]
self.i += n
return out
def items(self, n, first=0):
return slotted(self.take(n), first)
# ------------------------------------------------------------------ block entities
def sign_texts():
"""(a function: `js` depends on the era)"""
return [
[js({'text': 'Hello'}), js({'text': 'World', 'color': 'red', 'bold': True}), js({'text': ''}), js({'text': 'x', 'extra': [{'text': 'a', 'italic': True}]})],
[js('plain string'), js({'text': 'click', 'clickEvent': {'action': 'run_command', 'value': '/say hi'}}),
js({'text': 'hover', 'hoverEvent': {'action': 'show_text', 'value': {'text': 'tip'}}}), js({'text': 'selector', 'extra': [{'selector': '@p'}]})],
[js({'translate': 'chat.type.text', 'with': ['a', 'b']}), js({'score': {'name': 'Upgrader', 'objective': 'kills'}}), js([{'text': 'l1'}, {'text': 'l2'}]), js('')],
[q(''), q(''), q(''), q('')],
]
SKULL_PROFILE = ('Owner:{Id:"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6",Name:"Dinnerbone",Properties:{textures:[{Value:"eyJ0ZXh0dXJlcyI6e319"}]}}')
COMMANDS = ['say hi', 'tp @p ~ ~1 ~', 'execute @e[type=Zombie] ~ ~ ~ detect ~ ~-1 ~ stone 0 say hi', 'scoreboard players set @a kills 1',
'give @p minecraft:wool 1 5', 'setblock ~ ~1 ~ minecraft:stone', 'testfor @a', 'playsound minecraft:entity.pig.ambient master @a']
def tag_for(name, data, k, stacks):
"""The tile entity data for one placed block, or None."""
if name in ('chest', 'trapped_chest'):
t = 'Items:' + stacks.items(27 if k % 4 else 20)
if k % 5 == 0:
t += ',CustomName:' + q('Chest %d' % k)
if k % 7 == 3:
t += ',Lock:"secret"'
return t
if name in ('furnace', 'lit_furnace'):
t = 'Items:' + slotted(stacks.take(3)) + (',BurnTime:%ds,CookTime:%ds,CookTimeTotal:200s' % ((32000 if name == 'lit_furnace' else 0), 10 + k))
return t + (',CustomName:"Burner"' if k % 2 else '') + (',RecipesUsed:{"minecraft:iron_ingot":2}' if k % 3 == 0 else '')
if name in ('dispenser', 'dropper'):
return 'Items:' + stacks.items(9) + (',CustomName:%s' % q(name + ' ' + str(k)) if k % 2 else '')
if name == 'hopper':
return 'Items:' + stacks.items(5) + ',TransferCooldown:%d' % (k % 8) + (',CustomName:"Funnel"' if k % 2 else '')
if name == 'brewing_stand': # the block data follows the bottles in the first three slots
picks = stacks.take(3)
slots = ['{Slot:%db,%s' % (i, picks[i][1:]) for i in range(3) if data >> i & 1]
slots += ['{Slot:3b,%s' % stacks.take(1)[0][1:], '{Slot:4b,%s' % item('blaze_powder', 3)[1:]]
return ('Items:[' + ','.join(slots) + ']' + ',BrewTime:%ds,Fuel:%db' % (k * 3, 20 - k)
+ (',CustomName:"Brewery"' if k % 2 else ''))
if name == 'enchanting_table':
return 'CustomName:' + q('Enchant ' + str(k))
if name == 'beacon':
return 'Levels:%d,Primary:%d,Secondary:%d' % (k % 5, 1 + k % 10, k % 3 and 1 + k % 10)
if name == 'jukebox':
return ('RecordItem:' + item(RECORDS[k % len(RECORDS)])) if data else 'Record:%d' % (2256 + k % 12)
if name == 'noteblock':
return 'note:%db,powered:%db' % (k % 25, k % 2)
if name in ('command_block', 'repeating_command_block', 'chain_command_block'):
t = 'Command:%s,SuccessCount:%d,TrackOutput:%db,powered:0b,auto:0b,conditionMet:%db' % (
q(COMMANDS[k % len(COMMANDS)]), k % 4, k % 2, data // 8)
t += ',LastOutput:%s' % js({'text': '[@] done', 'color': 'white'}) if k % 2 else ''
t += ',CustomName:%s' % q('Cmd ' + str(k)) if k % 3 == 0 else ''
return t + ',UpdateLastExecution:1b,LastExecution:%dL' % (1000 + k)
if name == 'structure_block':
return ('name:"scene:test%d",author:"Me",metadata:"meta",posX:%d,posY:%d,posZ:%d,sizeX:%d,sizeY:%d,sizeZ:%d,rotation:%s,mirror:%s,'
'mode:%s,integrity:%d.5f,seed:%dL,ignoreEntities:%db,powered:0b,showair:%db,showboundingbox:1b') % (
k, k % 3 - 1, 1 + k % 2, -k % 3, 2 + k % 5, 3, 4, q(['NONE', 'CLOCKWISE_90', 'CLOCKWISE_180', 'COUNTERCLOCKWISE_90'][k % 4]),
q(['NONE', 'LEFT_RIGHT', 'FRONT_BACK'][k % 3]), q(['SAVE', 'LOAD', 'CORNER', 'DATA'][data & 3]), k % 2, k * 77, k % 2, k % 2)
if name in ('standing_sign', 'wall_sign'):
texts = sign_texts()
text = texts[k % len(texts)]
return ','.join('Text%d:%s' % (i + 1, text[i]) for i in range(4))
if name == 'mob_spawner':
mob = [m for m in MOBS if entity_form(m)][k % len([m for m in MOBS if entity_form(m)])]
spawn = 'SpawnData:{id:%s}' % (q(OLD_NAMES.get(mob, mob.title())) if k % 2 else eid(mob))
if k % 3 == 0:
spawn = ('SpawnData:{id:"minecraft:zombie",HandItems:[%s,{}],ArmorItems:[{},{},{},%s],CustomName:"Spawned",Health:5.0f},'
'SpawnPotentials:[{Entity:{id:"minecraft:skeleton",HandItems:[%s,{}]},Weight:2},{Entity:{id:"Pig"},Weight:1}]'
% (item('stone_sword', damage=3), item('log', damage=2), item('air', numeric=261)))
return (spawn + ',MaxNearbyEntities:%ds,RequiredPlayerRange:1s,SpawnCount:%ds,SpawnRange:%ds,Delay:%ds,MinSpawnDelay:%ds,MaxSpawnDelay:%ds'
% (6 + k % 3, 3 + k % 4, 4 + k % 2, 20 + k, 200, 800))
if name.endswith('shulker_box'):
return 'Items:' + stacks.items(27 if k % 2 else 12) + (',CustomName:%s' % q('Shulk ' + str(k)) if k % 3 == 0 else '') + (
',Lock:"k"' if k % 5 == 1 else '')
if name == 'end_gateway':
return 'Age:%dL,ExactTeleport:%db,ExitPortal:{X:%d,Y:%d,Z:%d}' % (k * 10, k % 2, 10 + k, 70, 20 - k)
if name in ('unpowered_comparator', 'powered_comparator'):
return 'OutputSignal:%d' % (k % 16)
if name == 'skull':
return None # see skull()
return None
# ------------------------------------------------------------------ the block sweep
# families the sweep does not place with the generic rule
SPECIAL = set(['air', 'bed', 'wooden_door', 'iron_door', 'spruce_door', 'birch_door', 'jungle_door', 'acacia_door', 'dark_oak_door',
'double_plant', 'standing_banner', 'wall_banner', 'skull', 'flower_pot', 'piston_head', 'piston_extension',
'daylight_detector', 'daylight_detector_inverted', 'observer', 'water', 'flowing_water', 'lava', 'flowing_lava',
'fire', 'portal', 'frosted_ice', 'lit_redstone_lamp', 'hopper'])
# the game recomputes these when they are added: place the block with data 0 first, then the real state (the second
# placement replaces a block of the same kind and does not run `onBlockAdded`)
TWO_STEP = set(['rail', 'golden_rail', 'detector_rail', 'activator_rail', 'redstone_wire', 'unpowered_repeater', 'powered_repeater',
'unpowered_comparator', 'powered_comparator', 'dispenser', 'dropper', 'piston', 'sticky_piston', 'tripwire_hook',
'tripwire', 'observer', 'daylight_detector', 'daylight_detector_inverted'])
# placed in the last tick: their state is rewritten by a scheduled tick
LATE_NAMES = set(['powered_repeater', 'powered_comparator'])
# soil under plants: name -> (block, data)
FLOORS = {
'sapling': ('grass', 0), 'tallgrass': ('grass', 0), 'red_flower': ('grass', 0), 'yellow_flower': ('grass', 0),
'wheat': ('farmland', 7), 'carrots': ('farmland', 7), 'potatoes': ('farmland', 7), 'beetroots': ('farmland', 7),
'pumpkin_stem': ('farmland', 7), 'melon_stem': ('farmland', 7), 'nether_wart': ('soul_sand', 0), 'cactus': ('sand', 0),
'deadbush': ('sand', 1), 'brown_mushroom': ('dirt', 2), 'red_mushroom': ('dirt', 2), 'waterlily': ('water', 0),
'reeds': ('sand', 0), 'chorus_flower': ('end_stone', 0), 'chorus_plant': ('end_stone', 0),
}
# blocks that need something solid at their side or above: surrounded by the supports
POCKETS = {'torch': 'stone', 'redstone_torch': 'stone', 'lever': 'stone', 'stone_button': 'stone', 'wooden_button': 'stone',
'ladder': 'stone', 'wall_sign': 'stone', 'wall_banner': 'stone', 'vine': 'stone', 'tripwire_hook': 'stone',
'cocoa': 'log'}
def pocket(scene, x, y, z, support):
block, data = ('stone', 0) if support == 'stone' else ('log', 3)
for dx, dy, dz in ((1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1), (0, 1, 0)):
scene.block(x + dx, y + dy, z + dz, block, data, expect=False)
def sweep_blocks(scene, stacks):
plain = Packer(scene.layers, stride=2, zstep=3)
pockets = Packer(scene.layers, stride=3, zstep=4)
k = 0
for name in sorted(BLOCK_STATES, key=lambda n: BLOCK_IDS[n]):
if name in SPECIAL or not block_ok(name):
continue
datas = BLOCK_STATES[name]
pk = name in POCKETS
packer = pockets if pk else plain
cells = packer.take(len(datas))
packer.gap()
for (x, y, z), d in zip(cells, datas):
k += 1
if name in FLOORS:
fb, fd = FLOORS[name]
scene.block(x, y - 1, z, fb, fd, expect=False)
if name == 'reeds':
scene.block(x + 1, y - 1, z, 'water', 0, expect=False)
if pk:
pocket(scene, x, y, z, POCKETS[name])
t = tag_for(name, d, k, stacks)
scene.block(x, y, z, name, d, '{' + t + '}' if t else None, late=name in LATE_NAMES)
# ------------------------------------------------------------------ the player: a bot that answers the server
def pos_long(x, y, z):
return struct.pack('>Q', ((x & 0x3FFFFFF) << 38) | ((y & 0xFFF) << 26) | (z & 0x3FFFFFF))
def make_player(Bot, varint, parse_varint, mcstring):
"""A Bot (protocol 340) that keeps the connection alive, confirms teleports and can act: dig, place, use, attack."""
class Player(Bot):
def __init__(self, *args):
self.entities = {} # entity id -> mob type id
self.pos = (0.0, 0.0, 0.0)
self.lock = threading.Lock()
super().__init__(*args)
def idle(self):
self.sock.settimeout(1.0)
while not self.stop.is_set():
try:
pid, data = self.receive()
except socket.timeout:
continue
except (EOFError, OSError):
break
try:
self.handle(pid, data)
except Exception:
pass
def handle(self, pid, data):
if self.protocol < 340: # 1.8.9 (47) to 1.11.2 (316): only stay joined
if self.protocol == 47 and pid == 0x00: # keep alive: a VarInt
self.send(0x00, data)
elif self.protocol > 47 and pid == 0x1F: # keep alive: a long
self.send(0x0B, data[:8])
elif self.protocol > 47 and pid == 0x2E: # player position and look: confirm the teleport
self.send(0x00, varint(parse_varint(data, 33)[1]))
return
if pid == 0x1F: # keep alive
self.send(0x0B, data[:8])
elif pid == 0x2F: # player position and look
x, y, z = struct.unpack('>ddd', data[:24])
self.pos = (x, y, z)
tid = parse_varint(data, 33)[1]
self.send(0x00, varint(tid))
self.send(0x0E, struct.pack('>dddffB', x, y, z, 0.0, 0.0, 1))
elif pid == 0x03: # spawn mob
p, eid = parse_varint(data, 0)
self.entities[eid] = data[p + 16] # skip the uuid, the type byte
def use_map_18(self, slot):
"""1.8 only: select a hotbar slot and right click in the air (an empty map there becomes a map)."""
self.send(0x09, struct.pack('>h', slot))
time.sleep(0.3)
self.send(0x08, struct.pack('>qBhBBB', -1, 255, -1, 0, 0, 0))
time.sleep(1.0)
def move(self, x, y, z):
self.pos = (x, y, z)
self.send(0x0D, struct.pack('>dddB', x, y, z, 1))
def held(self, slot):
self.send(0x1A, struct.pack('>h', slot))
def use_item(self, hand=0):
self.send(0x20, varint(hand))
def dig(self, x, y, z, face=1):
self.send(0x14, varint(0) + pos_long(x, y, z) + bytes([face]))
self.send(0x14, varint(2) + pos_long(x, y, z) + bytes([face]))
def place(self, x, y, z, face=1):
self.send(0x1F, pos_long(x, y, z) + varint(face) + varint(0) + struct.pack('>fff', 0.5, 1.0, 0.5))
def attack(self, eid):
self.send(0x0A, varint(eid) + varint(1))
def swing(self):
self.send(0x1D, varint(0))
def chat(self, text):
self.send(0x02, mcstring(text))
def respawn(self):
self.send(0x03, varint(0))
return Player
# ------------------------------------------------------------------ multi-block and tile entity families
DOORS = ['wooden_door', 'spruce_door', 'birch_door', 'jungle_door', 'acacia_door', 'dark_oak_door', 'iron_door']
FACE_XZ = {0: (0, 1), 1: (-1, 0), 2: (0, -1), 3: (1, 0)} # horizontal index -> (dx, dz)
FACE_XYZ = {0: (0, -1, 0), 1: (0, 1, 0), 2: (0, 0, -1), 3: (0, 0, 1), 4: (-1, 0, 0), 5: (1, 0, 0)} # piston style facing
def doors(scene):
pk = Packer(scene.layers, stride=2, zstep=4)
for name in DOORS:
for lower in range(8):
for upper in (8, 9, 10, 11):
x, y, z = pk.cell()
scene.block(x, y, z, name, lower)
scene.block(x, y + 1, z, name, upper)
for lower in (0, 5): # a door without its upper half
x, y, z = pk.cell()
scene.block(x, y, z, name, lower)
x, y, z = pk.cell() # and an upper half alone
scene.block(x, y + 1, z, name, 9)
pk.gap()
def double_plants(scene):
pk = Packer(scene.layers, stride=2, zstep=3)
for lower in range(6):
x, y, z = pk.cell()
scene.block(x, y - 1, z, 'grass', 0, expect=False)
scene.block(x, y, z, 'double_plant', lower)
scene.block(x, y + 1, z, 'double_plant', 8, expect=False)
x, y, z = pk.cell()
scene.block(x, y - 1, z, 'grass', 0, expect=False)
scene.block(x, y, z, 'double_plant', 4) # without upper half
x, y, z = pk.cell()
scene.block(x, y + 1, z, 'double_plant', 8, expect=False)
def beds(scene):
pk = Packer(scene.layers, stride=4, zstep=4)
k = 0
for color in range(16):
for f in range(4):
x, y, z = pk.cell()
dx, dz = FACE_XZ[f]
occupied = 4 if (color + f) % 5 == 0 else 0
scene.block(x, y, z, 'bed', f, '{color:%d}' % color)
scene.block(x + dx, y, z + dz, 'bed', 8 + f + occupied, '{color:%d}' % color)
k += 1
for f in range(4): # a foot or a head alone
x, y, z = pk.cell()
scene.block(x, y, z, 'bed', f, '{color:%d}' % (f * 3))
x, y, z = pk.cell()
scene.block(x, y, z, 'bed', 8 + f, '{color:%d}' % (f * 4 + 1))
def banner_tag(color, k):
n = k % 5
pats = ','.join('{Pattern:%s,Color:%d}' % (q(BANNER_PATTERNS[(color * 5 + k + j) % len(BANNER_PATTERNS)]), (color + 3 * j + k) % 16)
for j in range(n))
t = 'Base:%d' % color
if pats:
t += ',Patterns:[%s]' % pats
if k % 7 == 0:
t += ',CustomName:%s' % q('Banner %d' % k)
return '{%s}' % t
def banners(scene):
pk = Packer(scene.layers, stride=2, zstep=3)
pp = Packer(scene.layers, stride=3, zstep=4)
k = 0
for color in range(16):
for rot in range(16):
x, y, z = pk.cell()
scene.block(x, y, z, 'standing_banner', rot, banner_tag(color, k))
k += 1
for f in (2, 3, 4, 5):
x, y, z = pp.cell()
pocket(scene, x, y, z, 'stone')
scene.block(x, y, z, 'wall_banner', f, banner_tag(color, k))
k += 1
def skulls(scene):
pk = Packer(scene.layers, stride=2, zstep=3)
k = 0
for data in BLOCK_STATES['skull']:
for kind in range(6 if ERA >= 9 else 5):
for rot in (range(16) if data == 1 else [0] if data in (0, 8) else [k % 16]):
x, y, z = pk.cell()
tag = 'SkullType:%db,Rot:%db' % (kind, rot)
if kind == 3:
tag += ',' + SKULL_PROFILE if (k + rot) % 2 == 0 else ''
scene.block(x, y, z, 'skull', data, '{%s}' % tag)
k += 1
POT_CONTENTS = ([('', 0)] + [('red_flower', d) for d in range(9)] + [('yellow_flower', 0)] + [('sapling', d) for d in range(6)] +
[('red_mushroom', 0), ('brown_mushroom', 0), ('deadbush', 0), ('tallgrass', 2), ('cactus', 0), ('tallgrass', 0)])
def flower_pots(scene):
pk = Packer(scene.layers, stride=2, zstep=3)
for i in range(len(POT_CONTENTS) * 2):
content, data = POT_CONTENTS[i % len(POT_CONTENTS)]
x, y, z = pk.cell()
if i >= len(POT_CONTENTS) and content: # the second round names the item by its numeric id
it = str(BLOCK_IDS[content])
else:
it = q('minecraft:' + content if content else '')
scene.block(x, y, z, 'flower_pot', (i * 5) % 16 if i % 3 == 0 else 0, '{Item:%s,Data:%d}' % (it, data), expect=False)
def lamps_and_lit_things(scene):
pk = Packer(scene.layers, stride=3, zstep=3)
x, y, z = pk.cell()
scene.block(x + 1, y, z, 'redstone_block', 0)
scene.block(x, y, z, 'lit_redstone_lamp', 0)
x, y, z = pk.cell()
scene.block(x, y, z, 'redstone_lamp', 0)
x, y, z = pk.cell()
scene.block(x, y - 1, z, 'grass', 0, expect=False)
scene.block(x, y, z, 'pumpkin', 0)
x, y, z = pk.cell() # carved pumpkins on grass, dirt and podzol, snow on top (the flattening treats them apart)
for floor, fdata in (('grass', 0), ('dirt', 0), ('dirt', 2), ('mycelium', 0), ('grass', 0), ('dirt', 1)):
scene.block(x, y - 1, z, floor, fdata, expect=False)
scene.block(x, y, z, 'lit_pumpkin' if fdata else 'pumpkin', x % 4)
x, y, z = pk.cell()
for floor, fdata in (('grass', 0), ('dirt', 0), ('dirt', 2), ('mycelium', 0), ('grass', 0)):
scene.block(x, y - 1, z, floor, fdata, expect=False)
scene.block(x, y, z, 'snow_layer', 1 + x % 7)
x, y, z = pk.cell()
# snowy grass (a snow layer on top)
for i in range(4):
scene.block(x, y - 1, z, ['grass', 'dirt', 'mycelium', 'dirt'][i], [0, 2, 0, 0][i], expect=False)
scene.block(x, y, z, 'snow', 0)
x, y, z = pk.cell()
def notes_and_spawners(scene):
pk = Packer(scene.layers, stride=2, zstep=3)
for note in range(25):
for powered in (0, 1):
x, y, z = pk.cell()
scene.block(x, y, z, 'noteblock', 0, '{note:%db,powered:%db}' % (note, powered))
pk.gap()
for i in range(12):
x, y, z = pk.cell()
scene.block(x, y, z, 'mob_spawner', 0, '{%s}' % tag_for('mob_spawner', 0, i, None))
for i, rec in enumerate(RECORDS):
x, y, z = pk.cell()
scene.block(x, y, z, 'jukebox', 1, '{RecordItem:%s}' % item(rec))
x, y, z = pk.cell()
scene.block(x, y, z, 'jukebox', 0, '{Record:%d}' % (2256 + i))
def edges(scene):
"""The blocks that UpgradeData has to revisit, on both sides of chunk borders, in all four corners and edges."""
virtual = ['chest', 'trapped_chest', 'noteblock', 'bed', 'fire', 'oak_stairs', 'stone_stairs', 'brick_stairs', 'stone_brick_stairs',
'nether_brick_stairs', 'sandstone_stairs', 'spruce_stairs', 'birch_stairs', 'jungle_stairs', 'quartz_stairs',
'acacia_stairs', 'dark_oak_stairs', 'red_sandstone_stairs', 'purpur_stairs', 'redstone_wire', 'fence',
'nether_brick_fence', 'spruce_fence', 'birch_fence', 'jungle_fence', 'dark_oak_fence', 'acacia_fence',
'unpowered_repeater', 'powered_repeater', 'iron_bars', 'glass_pane', 'stained_glass_pane', 'fence_gate',
'spruce_fence_gate', 'birch_fence_gate', 'jungle_fence_gate', 'dark_oak_fence_gate', 'acacia_fence_gate',
'tripwire', 'cobblestone_wall', 'chorus_plant', 'melon_stem', 'pumpkin_stem', 'snow_layer', 'cauldron',
'standing_sign', 'mob_spawner']
virtual = [n for n in virtual if block_ok(n)]
y = scene.layers.take()
k = 0
cells = []
for chunk in range(1, 6): # borders x = 16 * (9 + chunk): x - 1 and x are on different chunks
bx = BX + 16 * chunk
for i in range(0, SIZE, 2):
cells.append((bx - 1, BZ + 1 + i)) # last column of a chunk, z odd
cells.append((bx, BZ + 2 + i)) # first column of the next chunk, z even
cells.append((BX + 1 + i, bz_of(chunk) - 1))
cells.append((BX + 2 + i, bz_of(chunk)))
seen = set()
for n, name in enumerate(virtual):
datas = BLOCK_STATES[name][:3]
for j, d in enumerate(datas):
for corner in range(8):
pos = cells[(n * 24 + j * 8 + corner * 31) % len(cells)]
while pos in seen or pos[1] >= BZ + SIZE - 1:
pos = (pos[0], (pos[1] + 2) if pos[1] + 2 < BZ + SIZE - 1 else BZ + 1 + (pos[1] % 2))
if pos in seen:
pos = (pos[0] + 2, pos[1])
seen.add(pos)
x, z = pos
if name in FLOORS:
scene.block(x, y - 1, z, FLOORS[name][0], FLOORS[name][1], expect=False)
t = tag_for(name, d, k, None) if name in ('standing_sign', 'wall_sign', 'mob_spawner') else None
if name in ('chest', 'trapped_chest'):
t = 'Items:[%s]' % item('wool', damage=k % 16, slot=0)
k += 1
if name == 'bed':
scene.block(x, y, z, 'bed', d % 4, '{color:%d}' % (k % 16))
continue
scene.block(x, y, z, name, d, '{%s}' % t if t else None, late=name in LATE_NAMES)
def bz_of(chunk):
return BZ + 16 * chunk
def transient_blocks(scene, stacks):
"""States the game rewrites within a tick or two: placed in the last tick, before the save."""
pk = Packer(scene.layers, stride=4, zstep=4)
late = True
# extended pistons, with their heads, powered from behind
for sticky in (0, 1):
for f in range(6):
x, y, z = pk.cell()
if f in (0, 1):
y += 3
scene.block(x, y - 1, z, 'stone', 0, late=late, expect=False)
dx, dy, dz = FACE_XYZ[f]
name = 'sticky_piston' if sticky else 'piston'
scene.block(x - dx, y - dy, z - dz, 'redstone_block', 0, late=late, expect=False)
scene.block(x, y, z, name, 8 + f, late=late)
scene.block(x + dx, y + dy, z + dz, 'piston_head', f + 8 * sticky, late=late)
# retracted pistons of every facing
for f in range(6):
for name in ('piston', 'sticky_piston'):
x, y, z = pk.cell()
scene.block(x, y, z, name, f, late=late)
# moving pistons
for i in range(12):
x, y, z = pk.cell()
f = i % 6
bid, bdata = [(1, 0), (35, 5), (54, 2), (17, 1), (5, 3), (159, 9), (95, 14), (44, 11), (155, 2), (24, 1), (3, 2), (12, 1)][i]
scene.block(x, y, z, 'piston_extension', [0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13][i],
'{blockId:%d,blockData:%d,facing:%d,progress:%s,extending:%db,source:%db}' % (
bid, bdata, f, ['0.0f', '0.5f', '1.0f'][i % 3], i % 2, (i // 2) % 2), late=late)
# daylight detectors
for name in ('daylight_detector', 'daylight_detector_inverted'):
for power in range(16):
x, y, z = pk.cell()
scene.block(x, y, z, name, power, late=late)
# hoppers, the disabled ones next to a power source
for data in BLOCK_STATES['hopper']:
x, y, z = pk.cell()
if data >= 8:
scene.block(x + 1, y, z, 'redstone_block', 0, late=late, expect=False)
scene.block(x, y, z, 'hopper', data, '{Items:%s,TransferCooldown:%d}' % (stacks.items(5), data % 4), late=late)
# observers, powered ones included
for data in range(6) if block_ok('observer') else (): # (the powered states only come from a pulse that takes ticks)
x, y, z = pk.cell()
scene.block(x, y, z, 'observer', data, late=late)
# liquids of every level, still and flowing
for name in ('water', 'flowing_water', 'lava', 'flowing_lava'):
for level in range(16):
x, y, z = pk.cell()
scene.block(x, y, z, name, level, late=late)
# fire of every age, frosted ice
for age in range(16):
x, y, z = pk.cell()
scene.block(x, y, z, 'fire', age, late=late)
for age in range(4) if block_ok('frosted_ice') else ():
x, y, z = pk.cell()
scene.block(x, y, z, 'frosted_ice', age, late=late)
# nether portals in both axes: obsidian frames, then the portal blocks with one fill
for axis in (1, 2):
x, y, z = pk.cell(span=2)
pk.gap()
if axis == 1:
scene.fill(x, y, z, x + 3, y + 4, z, 'obsidian', 0, late=late)
scene.fill(x + 1, y + 1, z, x + 2, y + 3, z, 'portal', 1, late=late)
scene.expect[(x + 1, y + 1, z)] = ('portal', 1)
else:
scene.fill(x, y, z, x, y + 4, z + 3, 'obsidian', 0, late=late)
scene.fill(x, y + 1, z + 1, x, y + 3, z + 2, 'portal', 2, late=late)
scene.expect[(x, y + 1, z + 1)] = ('portal', 2)
def top(scene, stacks):
"""The highest sections: a platform at y = 244 and columns of blocks up to the build limit."""
scene.layers.platform(244)
for i in range(16):
for y in range(240, 256):
scene.block(BX + 2 + 2 * i, y, BZ + 2, 'wool' if y % 2 else 'stained_hardened_clay', (i + y) % 16)
x, z = BX + 2, BZ + 6
scene.block(x, 255, z, 'chest', 2, '{Items:%s,CustomName:"Top chest"}' % stacks.items(27))
scene.block(x + 2, 255, z, 'standing_sign', 7, '{Text1:%s,Text2:%s}' % (js({'text': 'y=255'}), js({'text': 'limit'})))
scene.block(x + 4, 255, z, 'command_block', 1, '{Command:"say top",TrackOutput:0b}')
scene.block(x + 6, 254, z, 'skull', 1, '{SkullType:5b,Rot:4b}')
scene.block(x + 8, 253, z, 'bed', 1, '{color:14}')
scene.block(x + 7, 253, z, 'bed', 9, '{color:14}')
scene.block(x + 10, 255, z, 'standing_banner', 3, banner_tag(5, 3))
scene.block(x + 12, 250, z, 'shulker_box' if False else 'purple_shulker_box', 5, '{Items:%s}' % stacks.items(5))
scene.block(x + 14, 255, z, 'mob_spawner', 0, '{SpawnData:{id:%s},MaxNearbyEntities:6s,RequiredPlayerRange:1s}' % eid('ghast'))
for i in range(8): # and the very bottom of the world, next to the bedrock
scene.block(BX + 2 + 2 * i, 5 + i, BZ + 3, 'sponge', i % 2)
scene.block(BX + 2 + 2 * i, 1, BZ + 5, 'wool', i)
def build_blocks(scene, stacks):
sweep_blocks(scene, stacks)
doors(scene)
double_plants(scene)
beds(scene)
banners(scene)
skulls(scene)
flower_pots(scene)
lamps_and_lit_things(scene)
notes_and_spawners(scene)
edges(scene)
top(scene, stacks)
# ------------------------------------------------------------------ entities
def uuid_tag(n):
return 'UUIDMost:%dL,UUIDLeast:%dL' % (UUID_HI, n)
STAY = 'NoAI:1b,Silent:1b,NoGravity:1b,Invulnerable:1b,PersistenceRequired:1b'
def equipment(i, stacks=None):
"""Hand and armor items with numeric damage and enchantments, as 1.12 mobs carry them."""
weapons = ['diamond_sword', 'bow', 'iron_axe', 'golden_pickaxe', 'shield', 'fishing_rod', 'stone_hoe', 'wooden_shovel']
hand = item(weapons[i % 8], damage=i % 40, tag='{%s}' % ench(((i * 3) % 22, 1 + i % 3), (34, 2)) if i % 2 else None)
off = item('shield', damage=i % 5, tag='{BlockEntityTag:{Base:%d,Patterns:[{Pattern:"bs",Color:%d}]}}' % (i % 16, (i + 4) % 16)) if i % 3 == 0 else '{}'
armor = [item('leather_boots', damage=i % 20, tag='{%s}' % display(color=(i * 99991) % 16777216)),
item(['iron', 'golden', 'diamond', 'chainmail'][i % 4] + '_leggings', damage=i % 30),
item(['iron', 'golden', 'diamond', 'chainmail'][(i + 1) % 4] + '_chestplate', damage=i % 30, tag='{%s}' % ench((0, 2), (7, 3))),
(item('skull', damage=i % 6) if i % 4 == 0 else item('pumpkin') if i % 4 == 1 else item('wool', damage=i % 16) if i % 4 == 2 else item('diamond_helmet', damage=7, tag='{%s}' % ench((5, 2))))]
if ERA == 8: # before 1.9: one list, the held item first and then boots, leggings, chestplate and helmet
return 'Equipment:[%s,%s],DropChances:[0.085f,0.0f,1.0f,%.2ff,%.3ff]' % (hand, ','.join(armor), 0.1 * (i % 5), 0.05 * (i % 7))
return ('HandItems:[%s,%s],ArmorItems:[%s],HandDropChances:[0.085f,%.3ff],ArmorDropChances:[0.085f,0.0f,1.0f,%.2ff]'
% (hand, off, ','.join(armor), 0.05 * (i % 7), 0.1 * (i % 5)))
EFFECTS = 'ActiveEffects:[{Id:1b,Amplifier:1b,Duration:600,Ambient:0b,ShowParticles:1b},{Id:10b,Amplifier:0b,Duration:99999,Ambient:1b,ShowParticles:0b}]'
ATTRS = ('Attributes:[{Name:"generic.maxHealth",Base:30.0d,Modifiers:[{Name:"bonus",Amount:2.0d,Operation:0,UUIDMost:7L,UUIDLeast:8L}]},'
'{Name:"generic.movementSpeed",Base:0.2d},{Name:"generic.followRange",Base:40.0d}]')
TRADE_ITEMS = [('wheat', 0, 18), ('wool', 5, 1), ('dye', 4, 3), ('stained_hardened_clay', 11, 2), ('paper', 0, 24), ('coal', 1, 15),
('fish', 3, 6), ('cooked_fish', 1, 6), ('stone', 5, 8), ('log', 2, 4), ('record_13', 0, 1), ('golden_apple', 1, 1)]
def trades(n):
out = []
for i in range(n):
b = TRADE_ITEMS[i % len(TRADE_ITEMS)]
s = TRADE_ITEMS[(i * 5 + 3) % len(TRADE_ITEMS)]
t = '{buy:%s,sell:%s,maxUses:%d,uses:%d,rewardExp:%db' % (item(b[0], b[2], b[1]), item(s[0], 1, s[1]), 7 + i, i % 3, i % 2)
if i % 3 == 1:
t += ',buyB:%s' % item('emerald', 1)
if i % 4 == 2:
t = t.replace('sell:' + item(s[0], 1, s[1]), 'sell:' + item('enchanted_book', 1, 0, '{%s}' % stored_ench((34 + i % 2, 2), (70, 1))))
out.append(t + '}')
return 'Offers:{Recipes:[%s]}' % ','.join(out)
def entity_list(stacks):
"""(summon id, nbt, late). Fixed uuids are added by the caller."""
E = []
def add(kind, nbt='', late=False, stay=True):
E.append((kind, nbt, late, stay))
def ride(mount, mount_nbt, riders):
"""From 1.9 a mount lists its `Passengers`; before, the rider has a `Riding` mount (one rider only)."""
if ERA >= 9:
text = ','.join('{id:%s%s}' % (eid(k), (',' + n) if n else '') for k, n in riders)
add(mount, 'Passengers:[%s]' % text + (',' + mount_nbt if mount_nbt else ''))
else:
form = entity_form(mount)
mid = form[0] if form else mount
rider, rnbt = riders[0]
rform = entity_form(rider)
add(rider, (rnbt + ',' if rnbt else '') + 'Riding:{id:%s%s%s}' % (q(mid), (',' + form[1]) if form and form[1] else '',
(',' + mount_nbt) if mount_nbt else ''))
# monsters
for i, k in enumerate(['zombie', 'husk', 'skeleton', 'stray', 'wither_skeleton', 'creeper', 'spider', 'cave_spider', 'silverfish',
'endermite', 'blaze', 'ghast', 'witch', 'giant', 'guardian', 'elder_guardian', 'shulker', 'vex',
'evocation_illager', 'vindication_illager', 'illusion_illager', 'zombie_pigman', 'slime', 'magma_cube']):
extra = equipment(i)
if k == 'creeper':
extra += ',powered:1b,Fuse:30s,ExplosionRadius:4b,ignited:0b'
elif k in ('zombie', 'husk'):
extra += ',IsBaby:1b,CanBreakDoors:1b'
elif k == 'shulker':
extra = 'Color:%db,Peek:30b,AttachFace:0b,APX:1,APY:2,APZ:3' % (i % 16)
elif k == 'vex':
extra += ',BoundX:1,BoundY:2,BoundZ:3,LifeTicks:100'
elif k == 'zombie_pigman':
extra += ',Anger:200s,HurtBy:"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"'
elif k in ('slime', 'magma_cube'):
extra += ',Size:2'
elif k in ('ghast',):
extra = 'ExplosionPower:3'
elif k.endswith('illager') and k != 'evocation_illager':
extra += ',Johnny:1b' if k == 'vindication_illager' else ''
add(k, extra + ',' + EFFECTS + ',' + ATTRS + ',Health:%d.0f,CustomName:"%s",CustomNameVisible:%db,Tags:["scene","t%d"]' % (
5 + i, k.title() + ' ' + str(i), i % 2, i))
add('skeleton', 'SkeletonType:0b,' + equipment(3))
add('zombie', 'ZombieType:0,' + equipment(4))
add('zombie_villager', 'Profession:1,ConversionTime:-1,IsVillager:1b')
for prof in range(6):
add('zombie_villager', 'Profession:%d,ConversionTime:%d,IsBaby:%db,CustomName:"Zombie %d"' % (prof, [-1, 999999, -1][prof % 3], prof % 2, prof))
if ERA > 8:
add('zombie', 'IsVillager:1b,VillagerProfession:2')
# animals
for color in range(16):
add('sheep', 'Color:%db,Sheared:%db,Age:%d,InLove:0,%s' % (color, color % 2, -24000 if color % 3 == 0 else 0, ATTRS if color == 3 else 'Health:8.0f'))
for color in range(16):
add('wolf', 'CollarColor:%db,Sitting:%db,Angry:%db,%s' % (color, color % 2, color % 5 == 0, 'Owner:"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6",OwnerUUID:"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"' if color % 2 else 'Health:8.0f'))
for cat in range(4):
add('ocelot', 'CatType:%d,Sitting:%db,Owner:"Dinnerbone"' % (cat, cat % 2))
for r in (0, 1, 2, 3, 4, 5, 99):
add('rabbit', 'RabbitType:%d,MoreCarrotTicks:%d' % (r, r * 3))
for v in range(5):
add('parrot', 'Variant:%d,Sitting:%db,OwnerUUID:"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"' % (v, v % 2))
for kind in ('pig', 'cow', 'chicken', 'mooshroom', 'polar_bear', 'squid', 'bat', 'snowman', 'villager_golem'):
extra = equipment(5) if kind not in ('bat', 'squid') else ''
if kind == 'pig':
extra = 'Saddle:1b,' + extra
if kind == 'chicken':
extra = 'IsChickenJockey:0b,EggLayTime:5000,' + extra
if kind == 'bat':
extra = 'BatFlags:1b'
if kind == 'snowman':
extra = 'Pumpkin:1b'
if kind == 'villager_golem':
extra = 'PlayerCreated:1b'
add(kind, extra)
# horses
for v in (0, 1, 256, 513, 769, 1024, 1027, 4, 258):
add('horse', 'Variant:%d,Temper:%d,Tame:1b,Bred:%db,EatingHaystack:0b,HasReproduced:0b,OwnerUUID:"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6",'
'ArmorItem:%s,SaddleItem:%s' % (v, 50 + v % 40, v % 2, item(['iron', 'golden', 'diamond'][v % 3] + '_horse_armor'), item('saddle')))
add('donkey', 'ChestedHorse:1b,Items:%s,Tame:1b,SaddleItem:%s' % (slotted([item('wool', damage=3), item('apple'), item('dye', damage=15)], 2), item('saddle')))
add('mule', 'ChestedHorse:1b,Items:%s,Tame:0b' % slotted([item('log', damage=1, count=5)], 2))
add('skeleton_horse', 'SkeletonTrap:1b,SkeletonTrapTime:100,Tame:1b')
add('zombie_horse', 'Tame:1b')
for v in range(4):
add('llama', 'Variant:%d,Strength:%d,Tame:1b,ChestedHorse:1b,DecorItem:%s,Items:%s,Temper:20'
% (v, 1 + v, item('carpet', damage=v * 4 + 1), slotted([item('wool', damage=v), item('stone', damage=v)], 2)))
# villagers
for prof in range(6):
for career in (1, 2):
add('villager', 'Profession:%d,Career:%d,CareerLevel:%d,Riches:%d,Willing:%db,%s,Inventory:%s,Age:%d' % (
prof, career, 1 + career, prof * 5, prof % 2, trades(3 + prof), '[%s]' % item('wheat_seeds', 4) if prof % 2 else '[]', -24000 if prof == 3 else 0))
add('villager', 'Profession:0,Offers:{Recipes:[]}')
# armor stands
for i in range(10):
add('armor_stand', 'ShowArms:%db,Small:%db,NoBasePlate:%db,Invisible:%db,Marker:%db,DisabledSlots:%d,CustomName:"Stand %d",%s,'
'Pose:{Head:[%d.0f,10.0f,0.0f],Body:[0.0f,%d.0f,5.0f],LeftArm:[-10.0f,0.0f,-10.0f],RightLeg:[1.0f,2.0f,3.0f]}'
% (i % 2, (i // 2) % 2, i % 3 == 0, i % 4 == 0, i % 5 == 0, i * 64, i, equipment(i), i * 5, i * 3))
# dropped items and orbs
for i, s in enumerate([s for s in stacks.stacks if VALID_ITEMS is None or s.numeric is None][::37][:16]):
add('item', 'Item:%s,Age:100,PickupDelay:32767,Health:5,Owner:"Upgrader",Thrower:"Upgrader"' % s)
add('xp_orb', 'Value:50s,Age:100s,Health:5')
# minecarts and boats
add('minecart', 'CustomDisplayTile:1b,DisplayTile:"minecraft:stone",DisplayData:2,DisplayOffset:8,CustomName:"Cart"')
add('chest_minecart', 'Items:%s,DisplayTile:"minecraft:chest",DisplayData:0,DisplayOffset:6' % stacks.items(20))
add('chest_minecart', 'LootTable:"minecraft:chests/abandoned_mineshaft",LootTableSeed:7L')
add('furnace_minecart', 'PushX:1.0d,PushZ:0.0d,Fuel:3000s')
add('hopper_minecart', 'Items:%s,Enabled:0b,TransferCooldown:4' % stacks.items(5))
add('tnt_minecart', 'TNTFuse:100', late=True)
add('commandblock_minecart', 'Command:"say hello",LastOutput:%s,TrackOutput:1b,CustomName:"Commander"' % js({'text': '[@] hello'}))
add('spawner_minecart', 'SpawnData:{id:"minecraft:pig"},SpawnPotentials:[{Entity:{id:"minecraft:cow"},Weight:3}],SpawnCount:2s,Delay:100s,RequiredPlayerRange:1s')
for wood in WOODS:
add('boat', 'Type:%s' % q(wood))
ride('boat', '', [('villager', 'Profession:2'), ('pig', '')])
# riding stacks
ride('chicken', '', [('zombie', 'IsBaby:1b,' + equipment(2))])
ride('spider', '', [('skeleton', (('HandItems:[%s,{}]' if ERA >= 9 else 'Equipment:[%s,{},{},{},{}]') % item('bow')))])
ride('horse', 'Tame:1b', [('skeleton', 'Passengers:[{id:%s,Variant:3}]' % eid('parrot') if ERA >= 12 else '')])
add('pig', 'Leashed:1b,Leash:{UUIDMost:%dL,UUIDLeast:%dL}' % (UUID_HI, 99))
add('cow', 'Leashed:1b,Leash:{X:%d,Y:%d,Z:%d}' % (BX + 10, LAYERS[0], BZ + 10))
add('enderman', 'carried:"minecraft:grass",carriedData:0s,' + equipment(1))
add('enderman', 'carried:2s,carriedData:0s')
add('enderman', 'carried:"minecraft:log",carriedData:3s')
add('enderman', 'carried:35s,carriedData:5s')
add('enderman', 'carried:"minecraft:wool",carriedData:11s')
add('enderman', 'carried:"minecraft:stained_hardened_clay",carriedData:2s')
# the last tick: things that move, expire, or need a wall
for i, m in enumerate(['Kebab', 'Aztec', 'Alban', 'Aztec2', 'Bomb', 'Plant', 'Wasteland', 'Pool', 'Courbet', 'Sea', 'Sunset', 'Creebet',
'Wanderer', 'Graham', 'Match', 'Bust', 'Stage', 'Void', 'SkullAndRoses', 'Wither', 'Fighters', 'Pointer',
'Pigscene', 'BurningSkull', 'Skeleton', 'DonkeyKong']):
add('painting', 'Motive:%s,Facing:%db' % (q(m), i % 4), late=True, stay=False)
for i in range(10):
add('item_frame', 'Facing:%db,ItemRotation:%db,ItemDropChance:%d.0f,Item:%s' % (i % 4, i % 8, 1, item(
['wool', 'stained_hardened_clay', 'dye', 'filled_map', 'log', 'skull', 'banner', 'potion', 'fish', 'spawn_egg'][i], damage=i + 3)), late=True, stay=False)
add('item_frame', 'Facing:0b', late=True, stay=False)
for i, (blk, data, te) in enumerate([('sand', 0, None), ('gravel', 0, None), ('anvil', 5, None), ('chest', 2, 'Items:[%s]' % item('wool', 1, 4, slot=0)),
('log', 6, None), ('concrete_powder' if ERA == 12 else 'sand', 11, None), ('stained_hardened_clay', 14, None),
('standing_sign', 3, 'Text1:%s' % js({'text': 'falling'}))]):
add('falling_block', 'Block:"minecraft:%s",Data:%db,Time:1,DropItem:1b,HurtEntities:%db,FallHurtMax:40,FallHurtAmount:2.0f%s'
% (blk, data, i % 2, (',TileEntityData:{%s}' % te) if te else ''), late=True)
add('falling_block', 'TileID:12,Data:1b,Time:1,DropItem:1b', late=True)
add('falling_block', 'TileID:35,Data:14b,Time:1', late=True)
add('tnt', 'Fuse:80b', late=True)
for i, (kind, extra) in enumerate([
('arrow', 'inTile:"minecraft:stone",inData:0b,inGround:1b,xTile:1,yTile:2,zTile:3,life:10s,shake:2b,pickup:1b,player:1b,damage:2.0d,crit:1b'),
('arrow', 'inTile:"minecraft:planks",inData:3b,inGround:1b,pickup:2b'),
('arrow', 'inTile:5s,inData:2b,inGround:1b,xTile:4,yTile:5,zTile:6'),
('arrow', 'inTile:35,inData:7b,inGround:1b'),
('spectral_arrow', 'inTile:"minecraft:log",inData:2b,inGround:1b,Duration:100'),
('arrow', 'Potion:"minecraft:strong_poison",CustomPotionEffects:[{Id:1b,Amplifier:0b,Duration:100}],Color:255'),
('potion', 'Potion:%s' % (item('splash_potion', tag='{Potion:"minecraft:poison"}') if ERA >= 9 else item('potion', damage=16388))),
('potion', 'Potion:%s,inTile:0' % (item('lingering_potion', tag='{Potion:"minecraft:long_regeneration"}') if ERA >= 9
else item('potion', damage=16385))),
('potion', 'Potion:%s' % (item('splash_potion', damage=16453) if ERA >= 9 else item('potion', damage=16453))),
('egg', 'inTile:"minecraft:stone",inGround:1b'), ('snowball', 'inTile:"minecraft:dirt",inData:1b'),
('ender_pearl', 'inTile:"minecraft:stone"'), ('xp_bottle', ''), ('eye_of_ender_signal', ''),
('fireball', 'direction:[0.0d,-0.1d,0.0d],power:[0.0d,-0.1d,0.0d],ExplosionPower:2,inTile:"minecraft:netherrack"'),
('small_fireball', 'direction:[0.0d,0.0d,0.0d],power:[0.0d,0.0d,0.0d]'), ('wither_skull', 'Invulnerable:1b,direction:[0.0d,0.0d,0.0d]'),
('shulker_bullet', 'Steps:5,TXD:0.1d,TYD:0.0d,TZD:0.1d,Target:{L:5L,M:7L}'), ('llama_spit', ''), ('evocation_fangs', 'Warmup:5'),
('ender_crystal', 'ShowBottom:1b,BeamTarget:{X:%d,Y:%d,Z:%d}' % (BX, LAYERS[0], BZ)), ('ender_crystal', 'ShowBottom:0b'),
('fireworks_rocket', 'Life:0,LifeTime:5000,FireworksItem:%s' % item('fireworks', 1, 0, '{Fireworks:{Flight:1b,Explosions:[{Type:2b,Colors:[I;255]}]}}')),
('area_effect_cloud', 'Particle:"mobSpell",Radius:3.0f,Duration:600000,Age:3,WaitTime:20,ReapplicationDelay:20,DurationOnUse:0.0f,RadiusOnUse:0.0f,'
'RadiusPerTick:0.0f,Color:16711680,Potion:"minecraft:strong_healing",Effects:[{Id:1b,Amplifier:1b,Duration:100}]'),
('area_effect_cloud', 'Particle:"blockcrack",ParticleParam1:1,ParticleParam2:0,Radius:2.0f,Duration:600000,Potion:"minecraft:water"'),
('area_effect_cloud', 'Particle:"iconcrack",ParticleParam1:35,ParticleParam2:3,Radius:1.0f,Duration:600000'),
('area_effect_cloud', 'Particle:"reddust",Radius:1.0f,Duration:600000,Owner:"Dinnerbone"')]):
add(kind, extra, late=True)
return E
def summon_all(scene, stacks, late):
pk = Packer(scene.layers, stride=3, zstep=4)
for i, (kind, nbt, is_late, stay) in enumerate(entity_list(stacks)):
if is_late != late:
continue
form = entity_form(kind, i)
if form is None:
continue
x, y, z = pk.cell()
fields = uuid_tag(i + 1) + ',' + STAY
if form[1]:
fields += ',' + form[1]
if nbt:
fields += ',' + nbt
if kind in ('painting', 'item_frame', 'leash_knot'):
fields += ',TileX:%d,TileY:%d,TileZ:%d' % (x, y + 1, z)
scene.summon(form[0], x, y + 1, z, fields, late)
else:
scene.summon(form[0], x + 0.5, y, z + 0.5, fields, late)
def entities(scene, stacks):
summon_all(scene, stacks, False)
def late_content(scene, stacks):
"""Everything that is placed in the last tick, on platforms of its own (so that it can be wiped and redone)."""
scene.layers.fresh()
start = scene.layers.next
transient_blocks(scene, stacks)
summon_all(scene, stacks, True)
scene.late_layers = LAYERS[start:scene.layers.next]
scene.layers.fresh()
def wipe_late(scene):
"""Commands that remove the last tick's content again, if it has to be redone."""
cmds = []
for y in scene.late_layers:
cmds.append(f'kill @e[type=!player,x={BX},y={y - 2},z={BZ},dx={SIZE},dy=14,dz={SIZE}]')
for x in range(BX, BX + SIZE, 32):
for z in range(BZ, BZ + SIZE, 32):
cmds.append(f'fill {x} {y - 2} {z} {x + 31} {y + 11} {z + 31} minecraft:air 0')
cmds.append(f'fill {BX} {y - 2} {BZ} {BX + SIZE - 1} {y - 1} {BZ + SIZE - 1} minecraft:stone 0')
return cmds
# ------------------------------------------------------------------ player, scoreboard, advancements
PLAYER = 'Upgrader'
LAB = (BX + 56, 150, BZ + 107) # a stone platform in the air where the bot works; (x, floor y, z)
def inventory_commands(stacks):
cmds = []
pool = [s for s in stacks.stacks if not s.numeric and s.name != 'air']
# a hand-picked start of the hotbar, then the sweep
hot = [item('map'), item('map'), item('map'), item('iron_hoe', 1, 249), item('stone', 64), item('flint_and_steel', 1, 63),
item('wool', 64, 14), item('cobblestone', 32), item('torch', 20)]
if 8 < ERA < 12: # nobody uses the maps here: the server makes the map files of filled maps in a player's inventory
hot[:3] = [item('filled_map', 1, i) for i in range(3)]
for i, st in enumerate(hot):
cmds.append(f'replaceitem entity {PLAYER} slot.hotbar.{i} {st.command_args()}')
pool = [item('log', 16, 2), item('diamond_pickaxe', 1, 1560, '{%s}' % ench((32, 5), (34, 3)))] + pool
for i in range(26):
st = pool[(i * 41 + 7) % len(pool)] if i > 1 else pool[i]
cmds.append(f'replaceitem entity {PLAYER} slot.inventory.{i} {st.command_args()}')
armor = [('feet', item('leather_boots', 1, 12, '{%s}' % display('Boots', color=255))),
('legs', item('iron_leggings', 1, 20, '{%s}' % ench((0, 4), (34, 3)))),
('chest', item('elytra', 1, 100) if ERA >= 9 else item('diamond_chestplate', 1, 100)),
('head', item('skull', 1, 3, '{SkullOwner:{Id:"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6",Name:"Dinnerbone",'
'Properties:{textures:[{Value:"eyJ0ZXh0dXJlcyI6e319"}]}}}'))]
for slot, st in armor:
cmds.append(f'replaceitem entity {PLAYER} slot.armor.{slot} {st.command_args()}')
if ERA >= 9:
cmds.append(f'replaceitem entity {PLAYER} slot.weapon.offhand {item("shield", 1, 7, "{BlockEntityTag:{Base:11,Patterns:[{Pattern:%s,Color:3}]}}" % q("bs")).command_args()}')
for i in range(27):
st = pool[(i * 17 + 300) % len(pool)]
cmds.append(f'replaceitem entity {PLAYER} slot.enderchest.{i} {st.command_args()}')
return cmds
OBJECTIVES = [('dummy1', 'dummy', 'Dummy "quoted" objective'), ('kills', 'playerKillCount', 'Kills'), ('mined', 'stat.mineBlock.minecraft.stone', 'Mined stone'),
('crafted', 'stat.craftItem.minecraft.stick', None), ('usedsword', 'stat.useItem.minecraft.diamond_sword', 'Swung'),
('brokenpick', 'stat.breakItem.minecraft.iron_pickaxe', None), ('pickedup', 'stat.pickup.minecraft.dirt', None),
('dropped', 'stat.drop.minecraft.cobblestone', None), ('killedzombie', 'stat.killEntity.Zombie', 'Zombies'),
('killedbyskel', 'stat.entityKilledBy.Skeleton', None), ('walked', 'stat.walkOneCm', 'Walk'), ('jumps', 'stat.jump', None),
('kbt_red', 'killedByTeam.red', None), ('tk_blue', 'teamkill.blue', None), ('hp', 'health', 'Health'), ('foodlvl', 'food', None),
('airlvl', 'air', None), ('armorlvl', 'armor', None), ('xplvl', 'xp', None), ('levels', 'level', None), ('deaths', 'deathCount', 'Deaths'),
('totalkills', 'totalKillCount', None), ('trig', 'trigger', 'A trigger'), ('chest_open', 'stat.chestOpened', None),
('mine_log', 'stat.mineBlock.minecraft.log', None), ('killed_pig', 'stat.killEntity.Pig', None)]
def objectives():
"""The objectives this version has (1.8 lacks the food, air, armor, xp and level criteria and the pickup and drop statistics)."""
if ERA >= 9:
return OBJECTIVES
return [o for o in OBJECTIVES if o[1] not in ('food', 'air', 'armor', 'xp', 'level') and not o[1].startswith(('stat.pickup', 'stat.drop'))]
def scoreboard_commands():
cmds = []
for name, crit, disp in objectives():
cmds.append(f'scoreboard objectives add {name} {crit}' + (f' {disp}' if disp else ''))
cmds += ['scoreboard objectives setdisplay sidebar kills', 'scoreboard objectives setdisplay list hp',
'scoreboard objectives setdisplay belowName deaths', 'scoreboard objectives setdisplay sidebar.team.red mined',
'scoreboard objectives setdisplay sidebar.team.dark_blue walked']
colors = ['black', 'dark_blue', 'dark_green', 'dark_aqua', 'dark_red', 'dark_purple', 'gold', 'gray', 'dark_gray', 'blue', 'green',
'aqua', 'red', 'light_purple', 'yellow', 'white']
for i, c in enumerate(colors):
cmds.append(f'scoreboard teams add t_{c} Team {c.replace("_", " ").title()}')
cmds.append(f'scoreboard teams option t_{c} color {c}')
cmds.append(f'scoreboard teams option t_{c} friendlyfire {"true" if i % 2 else "false"}')
cmds.append(f'scoreboard teams option t_{c} seeFriendlyInvisibles {"true" if i % 3 else "false"}')
cmds.append(f'scoreboard teams option t_{c} nametagVisibility {["always", "never", "hideForOtherTeams", "hideForOwnTeam"][i % 4]}')
cmds.append(f'scoreboard teams option t_{c} deathMessageVisibility {["always", "never", "hideForOtherTeams", "hideForOwnTeam"][(i + 1) % 4]}')
if ERA >= 9:
cmds.append(f'scoreboard teams option t_{c} collisionRule {["always", "never", "pushOtherTeams", "pushOwnTeam"][i % 4]}')
cmds += ['scoreboard teams join t_red ' + PLAYER, 'scoreboard teams join t_blue Fakeplayer1 Fakeplayer2',
'scoreboard teams join t_gold Zombie']
for i, (name, crit, disp) in enumerate(objectives()):
if crit in ('health', 'food', 'air', 'armor', 'xp', 'level'):
continue # read-only criteria
cmds.append(f'scoreboard players set {PLAYER} {name} {i * 7 - 3}')
cmds.append(f'scoreboard players set Fakeplayer{1 + i % 2} {name} {i}')
tags = ['scoreboard players tag ' + PLAYER + ' add scene', 'scoreboard players tag ' + PLAYER + ' add second'] if ERA >= 9 else []
cmds += ['scoreboard players enable ' + PLAYER + ' trig', *tags, 'scoreboard players add ' + PLAYER + ' kills 5',
'scoreboard players operation ' + PLAYER + ' dummy1 = Fakeplayer1 dummy1']
return cmds
def advancement_commands():
biomes = ['forest', 'plains', 'ocean', 'river', 'taiga', 'roofed_forest', 'extreme_hills', 'swampland', 'beaches', 'savanna']
mobs = ['zombie', 'skeleton', 'spider', 'creeper', 'enderman', 'blaze', 'witch']
cmds = ['advancement grant ' + PLAYER + ' only minecraft:adventure/root',
'advancement grant ' + PLAYER + ' only minecraft:story/root',
'advancement grant ' + PLAYER + ' only minecraft:story/mine_stone',
'advancement grant ' + PLAYER + ' only minecraft:husbandry/root',
'advancement grant ' + PLAYER + ' until minecraft:story/upgrade_tools',
'advancement grant ' + PLAYER + ' only minecraft:nether/root',
'advancement grant ' + PLAYER + ' only minecraft:end/root']
cmds += ['advancement grant %s only minecraft:adventure/adventuring_time %s' % (PLAYER, b) for b in biomes]
cmds += ['advancement grant %s only minecraft:adventure/kill_all_mobs %s' % (PLAYER, m) for m in mobs]
cmds += ['advancement grant %s only minecraft:husbandry/bred_all_animals %s' % (PLAYER, m) for m in ('pig', 'cow', 'sheep')]
cmds += ['advancement grant %s only minecraft:husbandry/balanced_diet %s' % (PLAYER, m) for m in ('apple', 'bread', 'cooked_beef')]
cmds += ['recipe give %s *' % PLAYER]
return cmds
def achievement_commands():
"""Before 1.12: the achievements and the statistics are given by command."""
return [f'achievement give * {PLAYER}'] + [f'achievement take achievement.{a} {PLAYER}' for a in ('theEnd2', 'overpowered', 'snipeSkeleton')]
def lab_commands():
x, y, z = LAB
cmds = [f'fill {x - 6} {y - 1} {z - 6} {x + 6} {y - 1} {z + 6} minecraft:stone 0',
f'fill {x - 6} {y} {z - 6} {x + 6} {y + 4} {z + 6} minecraft:air 0']
return cmds
# ------------------------------------------------------------------ the bot works in the lab: real statistics and maps
def lab_actions(server, bot, problems):
"""Everything here is done by the bot with real packets, so the server records genuine statistics."""
x, y, z = LAB
pause = time.sleep
cx, cz = x + 0.5, z + 0.5
say = lambda cmds: batch(server, cmds, problems)
say(lab_commands() + [f'tp {PLAYER} {cx} {y} {cz}'])
pause(1.5)
# containers around the bot
ring = [(-3, -3, 'chest', 2), (-2, -3, 'trapped_chest', 2), (-1, -3, 'ender_chest', 2), (0, -3, 'furnace', 2), (1, -3, 'crafting_table', 0),
(2, -3, 'enchanting_table', 0), (3, -3, 'anvil', 0), (-3, 3, 'hopper', 0), (-2, 3, 'dispenser', 3), (-1, 3, 'dropper', 3),
(0, 3, 'brewing_stand', 0), (1, 3, 'beacon', 0), (2, 3, 'white_shulker_box', 1), (3, 3, 'jukebox', 0), (-3, -1, 'noteblock', 0),
(-3, 0, 'cake', 0), (-3, 1, 'cauldron', 1)]
say([f'setblock {x + dx} {y} {z + dz} minecraft:{n} {d} replace' for dx, dz, n, d in ring])
say([f'fill {x - 3} {y - 1} {z - 5} {x + 3} {y - 1} {z - 5} minecraft:grass 0',
f'setblock {x - 3} {y} {z - 5} minecraft:red_flower 0 replace', f'setblock {x - 2} {y} {z - 5} minecraft:yellow_flower 0 replace',
f'setblock {x - 1} {y} {z - 5} minecraft:tallgrass 1 replace', f'setblock {x} {y} {z - 5} minecraft:sapling 2 replace',
f'setblock {x + 1} {y} {z - 5} minecraft:red_flower 3 replace', f'setblock {x + 2} {y} {z - 5} minecraft:double_plant 0 replace',
f'setblock {x + 3} {y} {z - 5} minecraft:deadbush 0 replace'])
pause(1)
# open them all: right click with an empty hand
bot.held(8)
for dx, dz, n, d in ring:
bot.place(x + dx, y, z + dz, 1)
pause(0.25)
bot.send(0x08, varint0(0))
pause(0.5)
# note block: hit it, the cake is eaten by the right click above
bot.send(0x14, bytes([0]) + pos_long(x - 3, y, z - 1) + bytes([1]))
pause(0.3)
# walk to the plants and pick them: mined blocks, picked up items, walked distance
for i in range(7):
bot.move(x - 3 + i + 0.5, y, z - 4 + 0.5)
pause(0.2)
bot.dig(x - 3 + i, y, z - 5)
pause(0.4)
# walk, sneak, sprint and jump a little
px, pz = x + 0.5, z - 3.5
for i in range(24):
px += 0.2
bot.move(px, y, pz)
pause(0.06)
bot.send(0x15, varint0(3) + varint0(0) + varint0(0))
for i in range(24):
px -= 0.25
bot.move(px, y, pz)
pause(0.06)
bot.send(0x15, varint0(4) + varint0(0) + varint0(0))
bot.send(0x15, varint0(0) + varint0(0) + varint0(0))
for i in range(10):
px += 0.1
bot.move(px, y, pz)
pause(0.06)
bot.send(0x15, varint0(1) + varint0(0) + varint0(0))
for i in range(3):
bot.send(0x0D, struct.pack('>dddB', px, y + 0.42, pz, 0))
pause(0.15)
bot.send(0x0D, struct.pack('>dddB', px, y, pz, 1))
pause(0.4)
bot.move(cx, y, cz)
pause(0.3)
# place blocks with the items in the hotbar
for slot, target in ((4, (0, 1)), (6, (2, 1)), (7, (4, 1)), (8, (-2, 2))):
bot.held(slot)
pause(0.2)
for k in range(3):
bot.place(x + target[0] + k, y - 1, z + target[1] + 1, 1)
pause(0.2)
# a hoe used up, flint and steel used up
bot.held(3)
pause(0.2)
bot.place(x - 3, y - 1, z - 5, 1)
pause(0.4)
bot.held(5)
pause(0.2)
bot.place(x + 4, y - 1, z, 1)
pause(0.4)
# drop an item
bot.held(7)
pause(0.2)
bot.send(0x14, varint0(4) + pos_long(0, 0, 0) + bytes([0]))
pause(0.3)
# kill a few mobs
for kind, tid in (('pig', 90), ('zombie', 54), ('skeleton', 51), ('cow', 92), ('sheep', 91)):
say([f'summon {kind} {x + 1.5} {y} {z + 1.5} {{NoAI:1b,Health:1.0f,Tags:["labvictim"],PersistenceRequired:1b}}'])
pause(0.6)
eid = max([e for e, t in bot.entities.items() if t == tid], default=None) # the newest one is ours
if eid is not None:
for _ in range(3):
bot.attack(eid)
pause(0.4)
pause(0.3)
say(['kill @e[tag=labvictim]'])
# talk to a villager
say([f'summon villager {x + 1.5} {y} {z - 1.5} {{NoAI:1b,Profession:1,Career:1,Tags:["labvictim"]}}'])
pause(0.6)
eid = max([e for e, t in bot.entities.items() if t == 120], default=None)
if eid is not None:
bot.send(0x0A, varint0(eid) + varint0(0) + varint0(0))
pause(0.4)
bot.send(0x08, varint0(1))
say(['kill @e[tag=labvictim]'])
# the maps: slot 0, 1 and 2 hold an empty map each
for slot, (mx, mz) in enumerate(((x, z), (x - 80, z - 60), (x - 40, z - 20))):
if slot:
say([f'fill {mx - 2} {y - 1} {mz - 2} {mx + 2} {y - 1} {mz + 2} minecraft:stone 0', f'tp {PLAYER} {mx + 0.5} {y} {mz + 0.5}'])
pause(1.0)
bot.held(slot)
pause(0.2)
bot.use_item()
pause(5 if slot == 0 else 3)
bot.held(0)
say([f'tp {PLAYER} {cx} {y} {cz}'])
pause(1.5)
# die once, killed by a creeper (keepInventory keeps the player's things), and come back
say(['gamerule keepInventory true',
f'summon creeper {cx} {y} {cz} {{ignited:1b,Fuse:1s,ExplosionRadius:6b,PersistenceRequired:1b,Tags:["labvictim"]}}'])
pause(2.5)
bot.respawn()
pause(2.5)
say(['gamerule keepInventory false', 'kill @e[tag=labvictim]',
f'effect {PLAYER} minecraft:speed 99999 1 true', f'effect {PLAYER} minecraft:night_vision 99999 0 true',
f'effect {PLAYER} minecraft:regeneration 99999 0 false', f'tp {PLAYER} {BX}.5 {LAYERS[0]} {BZ}.5'])
pause(1.5)
def varint0(v):
out = b''
while True:
b = v & 0x7F
v >>= 7
out += bytes([b | (0x80 if v else 0)])
if not v:
return out
# Slow commands at the start of the last batch: while the server executes them the rest of the batch arrives, so that
# it all runs in one tick (the server runs queued console commands until its list is empty).
def spawn_origin(server, say):
"""Where to build: around the world spawn, whose chunks stay loaded (96 x 96 blocks, on chunk borders)."""
say(['save-all'])
time.sleep(2)
import anvil_dump
level = anvil_dump.read_dat(str(server.directory / 'world' / 'level.dat'))['Data']
return (level['SpawnX'] - 48) // 16 * 16, (level['SpawnZ'] - 48) // 16 * 16
def probe_items(server, say, names):
"""The item names this version knows (an unknown id is dropped when a chest loads its items)."""
x, y, z = BX + 1, 150, BZ + 1
cmds = [f'fill {x - 1} {y - 1} {z - 1} {x + 1} {y - 1} {z + 1} minecraft:stone 0']
for n in names:
cmds += [f'setblock {x} {y} {z} minecraft:chest 2 replace {{Items:[{{Slot:0b,id:"minecraft:{n}",Count:1b,Damage:0s}}]}}',
f'testforblock {x} {y} {z} minecraft:chest -1 {{Items:[{{id:"minecraft:{n}"}}]}}']
out = batch(server, cmds, [], quiet=True)
good = {n for n, o in zip(names, out[2::2]) if any('Successfully' in l for l in o)}
batch(server, [f'fill {x - 1} {y - 1} {z - 1} {x + 1} {y} {z + 1} minecraft:air 0'], [], quiet=True)
return good
def barrier():
return [f'fill {BX + dx} 108 {BZ + dz} {BX + dx + 31} 113 {BZ + dz + 31} minecraft:{b} 0'
for b in ('stone', 'air', 'stone', 'air') for dx in (0, 32, 64) for dz in (0, 32, 64)]
def set_origin(bx, bz):
"""Move the whole scene (the flat world has its spawn chunks elsewhere)."""
global BX, BZ, LAB
BX, BZ = bx, bz
LAB = (bx + 56, 150, bz + 107)
LAST = {}
def populate(server, Bot, port, protocol, name, problems):
"""Build the whole 1.12.2 scene, save it for good and return the (still joined) bot."""
harness = sys.modules[Bot.__module__]
Player = make_player(Bot, lab.varint, lab.parse_varint, harness.mcstring)
set_protocol(protocol)
say = lambda cmds, **kw: batch(server, cmds, problems, **kw)
if ERA < 12:
global VALID_ITEMS
VALID_ITEMS = None
origin = spawn_origin(server, say)
set_origin(*origin)
VALID_ITEMS = probe_items(server, say, ITEMS)
stacks = Stacks()
scene = Scene()
build_blocks(scene, stacks)
entities(scene, stacks)
late_content(scene, stacks)
LAST['scene'] = scene
say(['gamerule randomTickSpeed 0', 'gamerule doDaylightCycle false', 'gamerule doWeatherCycle false', 'gamerule doMobSpawning false',
'gamerule doFireTick false', 'gamerule mobGriefing false', 'gamerule sceneRule fixture', 'gamerule logAdminCommands true',
*(['gamerule announceAdvancements false', 'gamerule spawnRadius 7'] if ERA == 12 else []), 'time set 6000', 'weather clear 999999', 'difficulty 2',
'worldborder set 59999968', 'worldborder damage amount 0.25', 'worldborder warning distance 7'])
say(scene.cmds)
bot = Player(port, protocol, name)
time.sleep(3)
say(inventory_commands(stacks))
say([f'xp 117L {name}', f'xp 29 {name}',
*([f'effect {name} minecraft:speed 99999 1 true', f'effect {name} minecraft:night_vision 99999 0 true'] if ERA == 12 else
[f'effect {name} 1 99999 1 true', f'effect {name} 16 99999 0 true']),
f'spawnpoint {name} {BX} {LAYERS[0]} {BZ}', f'gamemode survival {name}'])
say(scoreboard_commands())
if ERA == 12:
say(advancement_commands())
try:
lab_actions(server, bot, problems)
except OSError as error: # the server dropped the bot
problems.append(('lab_actions', ['the bot lost its connection: %s' % error]))
else:
say(achievement_commands())
if ERA == 8: # 1.8 makes a map only when an empty map is used
for slot in range(3):
bot.use_map_18(slot)
bot.send(0x09, struct.pack('>h', 8))
time.sleep(3) # the inventory ticks once: the server creates the maps
time.sleep(2)
say(['save-all flush'])
bot.leave() # the player's data, statistics and advancements are written when the bot leaves
time.sleep(2)
# The last tick: transient states and entities, then the save. A normal stop would save the chunks again,
# after those states have changed (it ignores `save-off`), so the server is killed once the save is done.
# `time query gametime` before and after tells whether a tick ran in between (the server executes a queue of
# console commands in one tick unless the queue runs dry); if so the content is wiped and placed again.
slow = barrier()
batch_commands = ['time query gametime'] + slow + scene.late + ['save-all flush', 'time query gametime']
for attempt in range(5):
kept = len(problems)
if attempt:
say(wipe_late(scene))
out = say(batch_commands, chunk=len(batch_commands) + 5)
times = [m.group(1) for o in (out[0], out[-1]) for l in o for m in [re.search(r'[Tt]ime is (\d+)', l)] if m]
if len(times) == 2 and times[0] == times[1]:
break
del problems[kept:]
print(f' the last tick was split ({times}), again')
else:
problems.append(('last tick', ['the transient states were not saved in one tick']))
time.sleep(1)
server.process.kill()
server.process.wait()
try:
server.process.stdin.close()
except OSError:
pass
return bot
# ------------------------------------------------------------------ after the server stopped
HAND_STATS = {
# HAND-WRITTEN FIXTURE: statistics that the passive bot cannot cause (they are added only where the real
# server's file lacks the key), in the exact key format of a 1.12.2 stats file.
'stat.craftItem.minecraft.planks': 12, 'stat.craftItem.minecraft.stick': 8, 'stat.craftItem.minecraft.crafting_table': 1,
'stat.craftItem.minecraft.wooden_pickaxe': 1, 'stat.craftItem.minecraft.spawn_egg': 2, 'stat.craftItem.minecraft.dye': 3,
'stat.craftItem.minecraft.stained_hardened_clay': 4, 'stat.useItem.minecraft.stone': 40, 'stat.useItem.minecraft.log': 6,
'stat.useItem.minecraft.wooden_pickaxe': 17, 'stat.useItem.minecraft.spawn_egg': 3, 'stat.useItem.minecraft.bed': 1,
'stat.mineBlock.minecraft.stone': 55, 'stat.mineBlock.minecraft.log': 14, 'stat.mineBlock.minecraft.log2': 3,
'stat.mineBlock.minecraft.dirt': 31, 'stat.mineBlock.minecraft.grass': 9, 'stat.mineBlock.minecraft.leaves': 22,
'stat.mineBlock.minecraft.coal_ore': 4, 'stat.mineBlock.minecraft.iron_ore': 2, 'stat.mineBlock.minecraft.wool': 3,
'stat.mineBlock.minecraft.stained_hardened_clay': 1, 'stat.mineBlock.minecraft.double_plant': 2,
'stat.breakItem.minecraft.wooden_pickaxe': 1, 'stat.breakItem.minecraft.diamond_pickaxe': 1, 'stat.breakItem.minecraft.shears': 1,
'stat.breakItem.minecraft.spawn_egg': 1, 'stat.pickup.minecraft.dirt': 31, 'stat.pickup.minecraft.log': 14, 'stat.pickup.minecraft.cobblestone': 50,
'stat.pickup.minecraft.spawn_egg': 1, 'stat.pickup.minecraft.dye': 2, 'stat.drop.minecraft.cobblestone': 5, 'stat.drop.minecraft.dirt': 2,
'stat.drop.minecraft.spawn_egg': 1, 'stat.killEntity.Zombie': 7, 'stat.killEntity.Skeleton': 3, 'stat.killEntity.Creeper': 1,
'stat.killEntity.PigZombie': 1, 'stat.killEntity.LavaSlime': 2, 'stat.killEntity.MushroomCow': 1, 'stat.killEntity.Ozelot': 1,
'stat.killEntity.ZombieVillager': 1, 'stat.killEntity.EvocationIllager': 1, 'stat.entityKilledBy.Skeleton': 1, 'stat.entityKilledBy.Zombie': 2,
'stat.entityKilledBy.Creeper': 1, 'stat.entityKilledBy.Spider': 1, 'stat.entityKilledBy.VindicationIllager': 1,
'stat.walkOneCm': 8240, 'stat.crouchOneCm': 310, 'stat.sprintOneCm': 4500, 'stat.swimOneCm': 120, 'stat.fallOneCm': 1500,
'stat.climbOneCm': 220, 'stat.flyOneCm': 90, 'stat.diveOneCm': 60, 'stat.minecartOneCm': 700, 'stat.boatOneCm': 400, 'stat.pigOneCm': 30,
'stat.horseOneCm': 1200, 'stat.aviateOneCm': 800, 'stat.jump': 66, 'stat.damageDealt': 410, 'stat.damageTaken': 188, 'stat.deaths': 3,
'stat.mobKills': 17, 'stat.animalsBred': 4, 'stat.playerKills': 0, 'stat.fishCaught': 2, 'stat.talkedToVillager': 5,
'stat.tradedWithVillager': 2, 'stat.cakeSlicesEaten': 2, 'stat.cauldronFilled': 1, 'stat.cauldronUsed': 3, 'stat.armorCleaned': 1,
'stat.bannerCleaned': 2, 'stat.brewingstandInteraction': 4, 'stat.beaconInteraction': 1, 'stat.dropperInspected': 1,
'stat.hopperInspected': 2, 'stat.dispenserInspected': 3, 'stat.noteblockPlayed': 6, 'stat.noteblockTuned': 4, 'stat.flowerPotted': 2,
'stat.trappedChestTriggered': 1, 'stat.enderchestOpened': 3, 'stat.itemEnchanted': 2, 'stat.recordPlayed': 1,
'stat.furnaceInteraction': 9, 'stat.craftingTableInteraction': 12, 'stat.chestOpened': 15, 'stat.sleepInBed': 2, 'stat.shulkerBoxOpened': 1,
'stat.sneakTime': 400,
}
def fixtures(world):
"""Add what a real server could not be made to produce (see HAND_STATS), clearly apart from the real data."""
world = Path(world)
for f in sorted((world / 'stats').glob('*.json')):
data = json.loads(f.read_text())
for key, value in HAND_STATS.items():
data.setdefault(key, value)
f.write_text(json.dumps(data, separators=(',', ':')))
print(f' stats fixture: {len(data)} counters in {f.name}')
# ------------------------------------------------------------------ the flat world
FLAT_OPTIONS = '3;7,2x3,2;1;village,decoration' # 1.12 style: version;layers;biome;features
def build_flat(h, force=False):
"""`1.12.2-flat`: the same scene on a flat world (generator flat, the settings above), built with the harness
module `h` (tools/datafix_worlds.py)."""
version = '1.12.2'
jar, data_version, protocol = h.server_jar(version)
target = h.SHARED / 'worlds' / (version + '-flat')
if target.exists() and not force:
print(f'{version}-flat: kept {target}')
return []
work = h.LOCAL / 'gen' / (version + '-flat')
shutil.rmtree(work, ignore_errors=True)
port = lab.free_port()
class FlatServer(h.Server):
def __init__(self, directory): # as Server.__init__, with a flat world
directory.mkdir(parents=True, exist_ok=True)
(directory / 'eula.txt').write_text('eula=true\n')
props = {'server-port': port, 'online-mode': 'false', 'level-name': 'world', 'level-seed': '4242',
'view-distance': '4', 'spawn-protection': '0', 'enable-rcon': 'false', 'enable-query': 'false',
'max-tick-time': '-1', 'level-type': 'FLAT', 'generator-settings': FLAT_OPTIONS}
(directory / 'server.properties').write_text(''.join(f'{k}={v}\n' for k, v in props.items()))
self.process = h.subprocess.Popen([h.java(version), '-Xmx2G', '-jar', str(jar), 'nogui'], cwd=directory,
stdin=h.subprocess.PIPE, stdout=h.subprocess.PIPE, stderr=h.subprocess.STDOUT, text=True)
self.lines = []
self.queue = queue.Queue()
self.counter = 0
threading.Thread(target=self._pump, daemon=True).start()
self.wait('Done (', 600)
server = FlatServer(work)
problems = []
old = (BX, BZ)
set_origin(16, 16)
try:
# A flat world's spawn is found far away; make it (64, 64), and let a bot visit the scene's area so that its chunks
# are loaded (they stay loaded, being within the spawn radius).
batch(server, ['setworldspawn 64 4 64'], problems)
Player = make_player(h.Bot, lab.varint, lab.parse_varint, h.mcstring)
visitor = Player(port, protocol, h.BOT)
time.sleep(2)
for x, z in ((24, 24), (24, 72), (24, 104), (72, 24), (72, 72), (72, 104), (104, 24), (104, 72), (104, 104), (72, 123), (-8, 63), (32, 103)):
batch(server, [f'tp {h.BOT} {x}.5 4 {z}.5'], problems)
time.sleep(1.5)
batch(server, [f'tp {h.BOT} 64.5 4 64.5'], problems)
time.sleep(1)
visitor.leave()
time.sleep(1)
bot = populate(server, h.Bot, port, protocol, h.BOT, problems)
bot.leave()
finally:
set_origin(*old)
server.stop()
fixtures(work / 'world')
partial = target.with_name(target.name + '.partial')
shutil.rmtree(partial, ignore_errors=True)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(work / 'world', partial)
(partial / 'datafix-source.json').write_text(json.dumps(
{'version': version, 'data_version': data_version, 'problems': problems, 'flat': FLAT_OPTIONS}, indent=1))
shutil.rmtree(target, ignore_errors=True)
partial.rename(target)
for command, out in problems:
print(f'{version}-flat: command failed: {command}\n {out}')
print(f'{version}-flat: DataVersion {data_version}, world in {target}')
return problems