874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
1933 lines
110 KiB
Python
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
|