Polish Gothic hall with lanterns, landscaping and checked edits
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""Add the architecture polish layer to the finished v1 scene, without I/O.
|
||||
|
||||
The input is scene.Voxels after the eighteen finishing corrections. V1 modules
|
||||
remain immutable; removed blocks disappear from this sparse voxel composition.
|
||||
"""
|
||||
|
||||
|
||||
def build(v):
|
||||
"""Restore the roof silhouette and add restrained Gothic stone/iron detail."""
|
||||
stone = "minecraft:stone_bricks"
|
||||
trim = "minecraft:polished_andesite"
|
||||
dark = "minecraft:deepslate_tiles"
|
||||
glass = "minecraft:gray_stained_glass"
|
||||
wall = ("minecraft:stone_brick_wall[east=none,north=none,south=none,"
|
||||
"up=true,waterlogged=false,west=none]")
|
||||
|
||||
def stair(material, facing, half="bottom"):
|
||||
return (f"minecraft:{material}_stairs[facing={facing},half={half},"
|
||||
"shape=straight,waterlogged=false]")
|
||||
|
||||
def bars(north=False, south=False, east=False, west=False):
|
||||
flags = {"east": east, "north": north, "south": south, "west": west}
|
||||
return "minecraft:iron_bars[" + ",".join(
|
||||
f"{name}={str(value).lower()}" for name, value in flags.items()
|
||||
) + ",waterlogged=false]"
|
||||
|
||||
def roof_height(x):
|
||||
return 43 - (abs(x - 19) * 16 + 13) // 14
|
||||
|
||||
# Use the actual input mask so a material replacement cannot fill an opening.
|
||||
# Preserve the six pale blocks of the heraldic cross on the north facade.
|
||||
crest = {(19, y, 10) for y in range(23, 27)} | {
|
||||
(x, 25, 10) for x in range(18, 21)
|
||||
}
|
||||
timber = {
|
||||
p: block for p, block in v.blocks.items()
|
||||
if 9 <= p[0] <= 29 and 28 <= p[1] <= 40 and 15 <= p[2] <= 54
|
||||
and (block.startswith("minecraft:dark_oak_log[")
|
||||
or block == "minecraft:dark_oak_planks")
|
||||
}
|
||||
for p, block in tuple(v.blocks.items()):
|
||||
if block == "minecraft:polished_diorite" and p not in crest:
|
||||
v.set(*p, trim)
|
||||
elif block == "minecraft:tinted_glass":
|
||||
v.set(*p, glass)
|
||||
elif (block == "minecraft:cut_sandstone"
|
||||
and 39 <= p[0] <= 41 and 34 <= p[1] <= 37 and 48 <= p[2] <= 50):
|
||||
v.set(*p, "minecraft:gold_block")
|
||||
|
||||
# Remove only the ten old dormer envelopes, then reconstruct the same
|
||||
# two-cell roof section. These envelopes exclude both gables and all trusses.
|
||||
for z in (18, 26, 34, 42, 50):
|
||||
for x1, x2 in ((10, 14), (24, 28)):
|
||||
v.clear(x1, 32, z - 2, x2, 40, z + 2)
|
||||
for x in range(x1, x2 + 1):
|
||||
high = roof_height(x)
|
||||
for y in (high - 1, high):
|
||||
if 32 <= y <= 40:
|
||||
block = dark if y < high else stair(
|
||||
"deepslate_tile", "east" if x < 19 else "west")
|
||||
v.box(x, y, z - 2, x, y, z + 2, block)
|
||||
|
||||
# Three narrow, low dormers per slope: a one-by-two recessed light, three
|
||||
# blocks of facade width, and a short roof merging into the original slope.
|
||||
for z in (22, 34, 46):
|
||||
for face, inner, back in ((10, 11, 13), (28, 27, 25)):
|
||||
x1, x2 = sorted((face, back))
|
||||
near = back - 1 if face < back else back + 1
|
||||
v.clear(min(face, near), 33, z, max(face, near), 34, z)
|
||||
for dz in (-1, 1):
|
||||
v.box(x1, 32, z + dz, x2, 34, z + dz, dark)
|
||||
facing = "south" if dz < 0 else "north"
|
||||
v.box(x1, 35, z + dz, x2, 35, z + dz,
|
||||
stair("deepslate_tile", facing))
|
||||
v.box(face, 33, z + dz, face, 34, z + dz, trim)
|
||||
v.set(face, 35, z + dz, stair("stone_brick", facing))
|
||||
v.box(x1, 36, z, x2, 36, z, dark)
|
||||
v.box(face, 32, z - 1, face, 32, z + 1, trim)
|
||||
v.box(inner, 33, z, inner, 34, z, glass)
|
||||
v.set(face, 35, z, stone)
|
||||
v.set(face, 36, z, wall)
|
||||
|
||||
# The old bright, chunky ridge is now a dark backing for one thin iron rail.
|
||||
v.box(19, 43, 14, 19, 43, 55, dark)
|
||||
for z in range(14, 56):
|
||||
v.set(19, 44, z, bars(north=z > 14, south=z < 55))
|
||||
|
||||
# Replace full-block mullions with slender rods in their original glazing
|
||||
# plane. Connected horizontal bars provide a clear transom without a grille.
|
||||
for center in (18, 26, 34, 42, 50):
|
||||
for inner, outside in ((8, 6), (30, 32)):
|
||||
for y in range(10, 21):
|
||||
v.set(inner, y, center, bars())
|
||||
for dz in range(-2, 3):
|
||||
v.set(inner, 15, center + dz,
|
||||
bars(north=dz > -2, south=dz < 2))
|
||||
# A bevel on the underside of each stepped arch reduces square caps.
|
||||
for dz, top in ((-2, 19), (-1, 21), (1, 21), (2, 19)):
|
||||
v.set(outside, top + 1, center + dz,
|
||||
stair("stone_brick", "north" if dz < 0 else "south", "top"))
|
||||
for x in (18, 20):
|
||||
for y in range(29, 37):
|
||||
v.set(x, y, 14, bars())
|
||||
for x in range(16, 23):
|
||||
v.set(x, 33, 14, bars(east=x < 22, west=x > 16))
|
||||
|
||||
# Taper the tips, retaining the full support blocks installed by finish v1.
|
||||
for x in (6, 32):
|
||||
for z in (14, 22, 30, 38, 46, 54):
|
||||
v.box(x, 32, z, x, 33, z, wall)
|
||||
for x in (7, 31):
|
||||
v.box(x, 36, 12, x, 37, 12, wall)
|
||||
for x in (35, 45):
|
||||
for z in (43, 55):
|
||||
v.box(x, 49, z, x, 50, z, wall)
|
||||
|
||||
# Two new dormer centers cross truss planes. Keep every original timber
|
||||
# voxel, including the inclined members, instead of cutting a frame for glass.
|
||||
for p, block in timber.items():
|
||||
v.set(*p, block)
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Reference-driven finishing layer; the original scene and its ledgers stay immutable."""
|
||||
from .scene import build_scene as original_scene
|
||||
from .polish_architecture import build as architecture
|
||||
|
||||
STONE = 'minecraft:stone_bricks'
|
||||
TRIM = 'minecraft:polished_andesite'
|
||||
LEAVES = 'minecraft:oak_leaves[distance=7,persistent=true,waterlogged=false]'
|
||||
CHAIN = 'minecraft:iron_chain[axis=y,waterlogged=false]'
|
||||
SLAB = 'minecraft:stone_brick_slab[type=bottom,waterlogged=false]'
|
||||
|
||||
|
||||
def stair(facing, half='top'):
|
||||
return f'minecraft:stone_brick_stairs[facing={facing},half={half},shape=straight,waterlogged=false]'
|
||||
|
||||
|
||||
def bars(north=False, east=False, south=False, west=False):
|
||||
return ('minecraft:iron_bars[' + ','.join(f'{key}={str(value).lower()}' for key, value in
|
||||
[('north', north), ('east', east), ('south', south), ('west', west)]) + ',waterlogged=false]')
|
||||
|
||||
|
||||
def finished_scene():
|
||||
v = original_scene()
|
||||
for x in (6, 32):
|
||||
for z in (14, 22, 30, 38, 46, 54):
|
||||
v.set(x, 30, z, STONE)
|
||||
for x in (7, 31):
|
||||
v.set(x, 34, 12, STONE)
|
||||
for z in range(36, 40):
|
||||
v.set(35, 8, z, stair('east', 'bottom'))
|
||||
return v
|
||||
|
||||
|
||||
def lamp(v, x, y, z, hanging=True):
|
||||
v.set(x, y, z, f'minecraft:lantern[hanging={str(hanging).lower()},waterlogged=false]')
|
||||
|
||||
|
||||
def lighting(v):
|
||||
v.stage = 'lanterns and interior lighting'
|
||||
# Portal fixtures attach to the existing masonry jambs.
|
||||
for x in (13, 25):
|
||||
v.set(x, 14, 9, stair('south'))
|
||||
v.set(x, 13, 9, CHAIN)
|
||||
lamp(v, x, 12, 9)
|
||||
for x, facing, bays in ((4, 'east', (14, 22, 30, 38, 46, 54)),
|
||||
(34, 'west', (14, 22, 30))):
|
||||
for z in bays:
|
||||
v.set(x, 16, z, stair(facing))
|
||||
v.set(x, 15, z, CHAIN)
|
||||
lamp(v, x, 14, z)
|
||||
for x in (43, 52):
|
||||
v.set(x, 8, 14, stair('south'))
|
||||
v.set(x, 7, 14, CHAIN)
|
||||
lamp(v, x, 6, 14)
|
||||
for z in (21, 29, 37):
|
||||
v.set(59, 14, z, TRIM)
|
||||
v.set(60, 14, z, stair('west'))
|
||||
v.set(60, 13, z, CHAIN)
|
||||
lamp(v, 60, 12, z)
|
||||
# Existing low piers become courtyard lamps, with a full supporting cap.
|
||||
for x in (8, 30, 35, 61):
|
||||
for z in (6, 59):
|
||||
v.set(x, 4, z, TRIM)
|
||||
lamp(v, x, 5, z, False)
|
||||
# Three chandeliers hang in the clear nave, below the preserved timber ties.
|
||||
for z in (22, 38, 46):
|
||||
v.box(19, 19, z, 19, 27, z, CHAIN)
|
||||
for dx in range(-2, 3):
|
||||
v.set(19 + dx, 18, z, bars(dx == 0, dx < 2, dx == 0, dx > -2))
|
||||
for dz in (-2, -1, 1, 2):
|
||||
v.set(19, 18, z + dz, bars(dz > -2, False, dz < 2, False))
|
||||
for x, zz in ((17, z), (21, z), (19, z - 2), (19, z + 2)):
|
||||
lamp(v, x, 17, zz)
|
||||
for x, facing in ((13, 'west'), (25, 'east')):
|
||||
for z in (22, 38, 46):
|
||||
v.set(x, 12, z, stair(facing))
|
||||
v.set(x, 11, z, CHAIN)
|
||||
lamp(v, x, 10, z)
|
||||
# Wing ground floor: lights hang from the existing floor beams.
|
||||
for x in (43, 50):
|
||||
for z in (23, 31):
|
||||
v.set(x, 6, z, CHAIN)
|
||||
lamp(v, x, 5, z)
|
||||
for z in (23, 35):
|
||||
v.box(38, 17, z, 57, 17, z, 'minecraft:spruce_log[axis=x]')
|
||||
v.box(47, 14, z, 47, 16, z, CHAIN)
|
||||
lamp(v, 47, 13, z)
|
||||
# Tower landings and the block-built bell receive a restrained warm light.
|
||||
for y in (7, 16, 25):
|
||||
v.box(40, y + 1, 46, 40, y + 2, 46, CHAIN)
|
||||
lamp(v, 40, y, 46)
|
||||
for x in (37, 43):
|
||||
v.box(x, 39, 49, x, 40, 49, CHAIN)
|
||||
lamp(v, x, 38, 49)
|
||||
|
||||
|
||||
def landscape(v):
|
||||
v.stage = 'planted stone terrace'
|
||||
|
||||
def bed(x1, z1, x2, z2, high=3):
|
||||
# Soil replaces only the known terrace surface, with a low dressed rim.
|
||||
for x in range(x1, x2 + 1):
|
||||
for z in range(z1, z2 + 1):
|
||||
edge = x in (x1, x2) or z in (z1, z2)
|
||||
if edge:
|
||||
if (x, 1, z) not in v.blocks:
|
||||
v.set(x, 1, z, SLAB)
|
||||
else:
|
||||
v.set(x, 0, z, 'minecraft:dirt')
|
||||
height = high - int((x * 7 + z * 11) % 5 == 0)
|
||||
for y in range(1, height + 1):
|
||||
if (x, y, z) not in v.blocks:
|
||||
v.set(x, y, z, LEAVES)
|
||||
|
||||
for z in (18, 26, 34, 42, 50):
|
||||
bed(1, z - 2, 5, z + 2, 2)
|
||||
for z in (21, 29, 37):
|
||||
bed(60, z - 2, 64, z + 2, 2)
|
||||
bed(5, 9, 12, 11, 2)
|
||||
bed(26, 9, 32, 11, 2)
|
||||
bed(39, 11, 43, 13, 2)
|
||||
bed(52, 11, 56, 13, 2)
|
||||
bed(9, 59, 28, 61, 2)
|
||||
bed(48, 45, 59, 47, 2)
|
||||
|
||||
def narrow_tree(x, z, height):
|
||||
# Persistent leaves cannot decay; every addition respects existing masonry.
|
||||
if (x, 0, z) not in v.blocks or v.blocks[(x, 0, z)].split('[')[0] in (
|
||||
'minecraft:smooth_stone', 'minecraft:polished_andesite', 'minecraft:stone_bricks'):
|
||||
v.set(x, 0, z, 'minecraft:moss_block')
|
||||
for y in range(1, height - 1):
|
||||
if (x, y, z) not in v.blocks or v.blocks[(x, y, z)] == LEAVES:
|
||||
v.set(x, y, z, 'minecraft:spruce_log[axis=y]')
|
||||
for y in range(2, height + 1):
|
||||
radius = 2 if y <= height - 4 else 1 if y <= height - 1 else 0
|
||||
for dx in range(-radius, radius + 1):
|
||||
for dz in range(-radius, radius + 1):
|
||||
if abs(dx) + abs(dz) > radius + 1:
|
||||
continue
|
||||
p = x + dx, y, z + dz
|
||||
if p not in v.blocks or v.blocks[p] == LEAVES:
|
||||
v.set(*p, LEAVES)
|
||||
for x, z, h in ((10, 9, 8), (28, 9, 8), (62, 16, 7), (62, 44, 7),
|
||||
(6, 60, 8), (32, 60, 7)):
|
||||
narrow_tree(x, z, h)
|
||||
# Slight wear follows the edges, leaving the main approach calm and readable.
|
||||
for (x, y, z), block in list(v.blocks.items()):
|
||||
if y == 0 and block == 'minecraft:smooth_stone' and (x < 6 or x > 59 or z > 57):
|
||||
if (x * 31 + z * 17) % 29 == 0:
|
||||
v.set(x, y, z, 'minecraft:andesite')
|
||||
if y in (1, 2, 3) and block == STONE and (x < 8 or x > 56) and (x * 13 + y * 7 + z * 19) % 31 == 0:
|
||||
v.set(x, y, z, 'minecraft:mossy_stone_bricks')
|
||||
|
||||
|
||||
def build_scene():
|
||||
v = finished_scene()
|
||||
architecture(v)
|
||||
lighting(v)
|
||||
landscape(v)
|
||||
return v
|
||||
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Plan, apply, or verify the Gothic hall polish against its saved canonical baseline."""
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
import fcntl
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
from builds.gothic_hall.scene import boxes
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUTPUT = ROOT / '.runtime/gothic-hall/polish'
|
||||
MANIFEST = OUTPUT / 'manifest.json'
|
||||
LEDGER = OUTPUT / 'ledger.json'
|
||||
AIR = 'minecraft:air'
|
||||
spec = importlib.util.spec_from_file_location('gothic_finish', ROOT / 'scripts/finish-gothic-hall.py')
|
||||
finish = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(finish)
|
||||
require, digest, point, position = finish.require, finish.digest, finish.point, finish.position
|
||||
|
||||
|
||||
class Backend(finish.Backend):
|
||||
def rpc(self, method, params):
|
||||
try:
|
||||
return super().rpc(method, params)
|
||||
except RuntimeError as error:
|
||||
raise RuntimeError(str(error).replace('finish ledger', 'polish ledger')) from None
|
||||
|
||||
|
||||
def matches(actual, requested):
|
||||
"""Match explicit requested properties while accepting registry defaults."""
|
||||
def split(state):
|
||||
name, _, suffix = state.partition('[')
|
||||
properties = dict(item.split('=', 1) for item in suffix.rstrip(']').split(',') if item)
|
||||
return name, properties
|
||||
actual_name, actual_properties = split(actual)
|
||||
requested_name, requested_properties = split(requested)
|
||||
return actual_name == requested_name and all(
|
||||
actual_properties.get(key) == value for key, value in requested_properties.items())
|
||||
|
||||
|
||||
def baseline():
|
||||
model, originals, plans = finish.references()
|
||||
patch = json.loads(finish.LEDGER.read_text())
|
||||
finish.validate_saved_finish(patch, finish.targets(model, originals), model, plans)
|
||||
states = dict(originals)
|
||||
states.update({point(c['pos']): c['desired'] for c in patch['changes']})
|
||||
return model, states, plans, patch
|
||||
|
||||
|
||||
def bounds(positions):
|
||||
lo = tuple(min(p[i] for p in positions) for i in range(3))
|
||||
hi = tuple(max(p[i] for p in positions) for i in range(3))
|
||||
require((hi[0] - lo[0] + 1) * (hi[1] - lo[1] + 1) * (hi[2] - lo[2] + 1) <= 2048,
|
||||
'Inspection volume exceeds the 16x8x16 tile budget')
|
||||
return {'min': position(lo), 'max': position(hi)}
|
||||
|
||||
|
||||
def manifest(model, states, plans, patch):
|
||||
from builds.gothic_hall.polish_scene import finished_scene, build_scene
|
||||
before, after = finished_scene().blocks, build_scene().blocks
|
||||
origin = model['origin']
|
||||
|
||||
def world(local):
|
||||
return tuple(a + b for a, b in zip(local, origin))
|
||||
|
||||
require({world(p) for p in before} == set(states), 'Finished scene baseline mask changed')
|
||||
require(all(matches(states[world(p)], value) for p, value in before.items()),
|
||||
'Finished scene differs from the original plans plus the 18 finish states')
|
||||
changes, tiles = {}, defaultdict(dict)
|
||||
for local in sorted(before.keys() | after.keys()):
|
||||
old, desired = before.get(local, AIR), after.get(local, AIR)
|
||||
if finish.canonical(old) == finish.canonical(desired):
|
||||
continue
|
||||
require(-2 <= local[0] <= 68 and -1 <= local[1] <= 64 and -2 <= local[2] <= 68,
|
||||
'Polish extends beyond the agreed local bounds')
|
||||
at = world(local)
|
||||
expected = states.get(at, AIR)
|
||||
# Different spellings that only make existing default properties explicit
|
||||
# have no world effect and are omitted before requesting a server plan.
|
||||
if finish.canonical(expected) == finish.canonical(desired):
|
||||
continue
|
||||
changes[local] = {'pos': position(at), 'expected': expected, 'desired': desired}
|
||||
tiles[(local[0] // 16, local[1] // 8, local[2] // 16)][local] = desired
|
||||
batches = []
|
||||
for tile, cells in sorted(tiles.items()):
|
||||
compressed = boxes(cells)
|
||||
for start in range(0, len(compressed), 256):
|
||||
operations, selected = [], []
|
||||
for x1, y1, z1, x2, y2, z2, block in compressed[start:start + 256]:
|
||||
operations.append({'type': 'box', 'min': position(world((x1, y1, z1))),
|
||||
'max': position(world((x2, y2, z2))), 'block': block})
|
||||
selected.extend(changes[(x, y, z)]
|
||||
for x in range(x1, x2 + 1) for y in range(y1, y2 + 1)
|
||||
for z in range(z1, z2 + 1))
|
||||
selected.sort(key=lambda c: point(c['pos']))
|
||||
require(len(selected) == len({point(c['pos']) for c in selected}) <= 2048,
|
||||
'Compressed batch contains duplicates or exceeds the block budget')
|
||||
batches.append({'key': '_'.join(map(str, (*tile, start // 256))),
|
||||
**bounds([point(c['pos']) for c in selected]), 'changes': selected,
|
||||
'recipe': {'version': 1, 'operations': operations}})
|
||||
require(sum(len(b['changes']) for b in batches) == len(changes), 'Incomplete polish batch mask')
|
||||
return {'version': 1, 'name': 'Gothic hall polish', 'origin': origin,
|
||||
'world_id': model['world_id'], 'world_epoch': model['world_epoch'],
|
||||
'baseline_manifest_sha256': digest(model), 'finish_ledger_sha256': digest(patch),
|
||||
'source_plans': {key: {'plan_id': value['plan_id'], 'sha256': value['sha256']}
|
||||
for key, value in plans.items()},
|
||||
'before_blocks': len(before), 'after_blocks': len(after), 'changes': len(changes),
|
||||
'added': sum(p not in before for p in changes),
|
||||
'removed': sum(p not in after for p in changes), 'batches': batches}
|
||||
|
||||
|
||||
def prepared(record, batch, model, scope):
|
||||
plan, checksum = finish.envelope(
|
||||
finish.CONFIG.parent / 'journal/plans' / (record['plan']['plan_id'] + '.json'), 'plan')
|
||||
require(checksum == record['plan']['plan_hash'], 'Prepared polish plan hash changed')
|
||||
require(plan['worldEpoch'] == model['world_epoch'] and plan['region']['worldId'] == model['world_id'],
|
||||
'Prepared polish world identity changed')
|
||||
require(plan['projectId'] == scope['project_id'], 'Prepared polish project changed')
|
||||
expected = {point(c['pos']): c for c in batch['changes']}
|
||||
actual = {point(c['pos']): c for c in plan['changes']}
|
||||
require(len(plan['changes']) == len(actual) == len(expected) and actual.keys() == expected.keys(),
|
||||
'Prepared polish mask differs from the reviewed diff')
|
||||
for at, change in actual.items():
|
||||
require(change['expected'] == expected[at]['expected'],
|
||||
f'Prepared expected state changed at world {at}; preserve current world')
|
||||
require(matches(change['desired'], expected[at]['desired']),
|
||||
f'Prepared desired properties differ at world {at}')
|
||||
require(record['idempotency_key'] == 'gothic-hall-polish-' + plan['id'],
|
||||
'Polish idempotency identity changed')
|
||||
return {at: c['desired'] for at, c in actual.items()}
|
||||
|
||||
|
||||
def inspect(backend, region, states, planter_soil=frozenset()):
|
||||
result = backend.rpc('region_inspect', {**region, 'detail': 'blocks'})
|
||||
live = {point(block['pos']): block['state'] for block in result['blocks']}
|
||||
variations = []
|
||||
for at, expected in states.items():
|
||||
# Verification may observe grass spreading over the newly planted soil.
|
||||
# Preparation and application pass no such allowance and remain exact.
|
||||
if at in planter_soil and expected == 'minecraft:dirt' and live.get(at) == 'minecraft:grass_block[snowy=false]':
|
||||
variations.append({'pos': position(at), 'expected': expected, 'observed': live[at]})
|
||||
continue
|
||||
require(live.get(at) == expected,
|
||||
f'Live state differs at world {at}; preserve current world and inspect before continuing')
|
||||
return variations
|
||||
|
||||
|
||||
def completed(backend, record, apply=False):
|
||||
ident = finish.operation_id(record)
|
||||
if ident is None and apply:
|
||||
# The record and key have already been fsynced. A transport exception
|
||||
# escapes immediately; a later invocation first inspects the journal.
|
||||
result = backend.rpc('build_apply', {**record['plan'], 'idempotency_key': record['idempotency_key']})
|
||||
ident = result['operation_id']
|
||||
require(ident is not None, 'A required operation has not been applied')
|
||||
for _ in range(600 if apply else 1):
|
||||
status = backend.rpc('operation_status', {'operation_id': ident})
|
||||
if status['status'] not in ('queued', 'applying'):
|
||||
break
|
||||
if apply:
|
||||
time.sleep(.1)
|
||||
require(status['status'] == 'applied',
|
||||
f'Operation {ident} stopped at {status["status"]}; inspect before continuing')
|
||||
return ident
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('action', choices=['plan', 'apply', 'verify'])
|
||||
args = parser.parse_args()
|
||||
model, states, source_plans, patch = baseline()
|
||||
wanted = manifest(model, states, source_plans, patch)
|
||||
if MANIFEST.exists():
|
||||
require(digest(json.loads(MANIFEST.read_text())) == digest(wanted),
|
||||
'Immutable polish manifest differs from the current generator or baseline')
|
||||
else:
|
||||
require(args.action == 'plan', 'Run plan before apply or verify')
|
||||
finish.save(MANIFEST, wanted, immutable=True)
|
||||
backend = Backend()
|
||||
context = backend.rpc('project_context', {})
|
||||
require(context['world_id'] == model['world_id'] and context['world_epoch'] == model['world_epoch'],
|
||||
'Live world identity changed')
|
||||
require(patch['scope'] == backend.scope, 'Finished baseline owner/project scope changed')
|
||||
completed(backend, patch)
|
||||
if LEDGER.exists():
|
||||
ledger = json.loads(LEDGER.read_text())
|
||||
else:
|
||||
require(args.action == 'plan', 'The polish ledger is missing; run plan first')
|
||||
ledger = {'version': 1, 'manifest_sha256': digest(wanted), 'scope': backend.scope, 'batches': {}}
|
||||
finish.save(LEDGER, ledger, immutable=True)
|
||||
require(ledger['version'] == 1 and ledger['manifest_sha256'] == digest(wanted)
|
||||
and ledger['scope'] == backend.scope, 'Polish ledger identity changed')
|
||||
require(set(ledger['batches']) <= {b['key'] for b in wanted['batches']}, 'Unknown polish ledger batch')
|
||||
canonical_targets = {}
|
||||
for batch in wanted['batches']:
|
||||
key = batch['key']
|
||||
if key not in ledger['batches']:
|
||||
require(args.action == 'plan', 'Some polish batches are not prepared; run plan first')
|
||||
inspect(backend, {'min': batch['min'], 'max': batch['max']},
|
||||
{point(c['pos']): c['expected'] for c in batch['changes']})
|
||||
summary = backend.rpc('build_prepare', {'recipe': batch['recipe']})
|
||||
record = {'plan': summary, 'idempotency_key': 'gothic-hall-polish-' + summary['plan_id']}
|
||||
prepared(record, batch, wanted, backend.scope)
|
||||
# Existing records are never replaced. Atomic publication follows
|
||||
# server-plan validation and always precedes any build_apply call.
|
||||
ledger['batches'][key] = record
|
||||
finish.save(LEDGER, ledger)
|
||||
canonical_targets.update(prepared(ledger['batches'][key], batch, wanted, backend.scope))
|
||||
if args.action == 'plan':
|
||||
print(json.dumps({'status': 'prepared', 'changes': wanted['changes'],
|
||||
'batches': len(wanted['batches']), 'ledger': str(LEDGER)}))
|
||||
return
|
||||
operations = []
|
||||
for index, batch in enumerate(wanted['batches'], 1):
|
||||
ident = completed(backend, ledger['batches'][batch['key']], apply=args.action == 'apply')
|
||||
operations.append(ident)
|
||||
if args.action == 'apply':
|
||||
inspect(backend, {'min': batch['min'], 'max': batch['max']},
|
||||
{point(c['pos']): canonical_targets[point(c['pos'])] for c in batch['changes']})
|
||||
print(json.dumps({'status': 'applied', 'batch': index, 'batches': len(wanted['batches']),
|
||||
'operation_id': ident}), flush=True)
|
||||
if args.action == 'apply':
|
||||
return
|
||||
final, tiles = {**states, **canonical_targets}, defaultdict(dict)
|
||||
for at, value in final.items():
|
||||
local = tuple(a - b for a, b in zip(at, wanted['origin']))
|
||||
tiles[(local[0] // 16, local[1] // 8, local[2] // 16)][at] = value
|
||||
planter_soil = frozenset(at for at, state in canonical_targets.items()
|
||||
if state == 'minecraft:dirt' and at[1] == wanted['origin'][1])
|
||||
soil_variations = []
|
||||
for _, group in sorted(tiles.items()):
|
||||
soil_variations.extend(inspect(backend, bounds(list(group)), group, planter_soil))
|
||||
report = {'status': 'verified', 'checked_positions': len(final),
|
||||
'blocks': sum(value != AIR for value in final.values()),
|
||||
'checked_air': sum(value == AIR for value in final.values()),
|
||||
'changes': wanted['changes'], 'inspection_tiles': len(tiles),
|
||||
'accepted_planter_grass': soil_variations,
|
||||
'manifest_sha256': digest(wanted), 'ledger_sha256': digest(ledger),
|
||||
'operations': operations, 'scope': backend.scope, 'verified_at': time.time()}
|
||||
finish.save(OUTPUT / 'verification.json', report)
|
||||
print(json.dumps(report))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
with (OUTPUT / '.lock').open('a') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
main()
|
||||
except (OSError, ValueError, KeyError, RuntimeError) as error:
|
||||
raise SystemExit('Polish stopped: ' + str(error)) from None
|
||||
Reference in New Issue
Block a user