Polish Gothic hall with lanterns, landscaping and checked edits

This commit is contained in:
Emil
2026-09-12 23:45:14 +03:00
parent 54af124a6d
commit f50fce2e55
13 changed files with 662 additions and 4 deletions
@@ -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)
+159
View File
@@ -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
+254
View File
@@ -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