Expose the runtime block/item registry through compact material search and single-material descriptions. Preserve private block-entity data through checked edits and durable undo without sending payloads to model context. Include the completed terrain tools, isolated world/map plugin, station lift, ACP streaming and guidance fixes, local camera auto-connect, construction scripts, and their public documentation, references and verification records. Active station decoration and private runtime data remain outside this commit. Validation: 145 Maven tests, 47 Bridge tests, successful camera Gradle build, and isolated Paper verification of all 1,196 block defaults plus 5,392 independent property cases for placement, same-material edits and restoration.
172 lines
9.4 KiB
Python
172 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Compile a continuous, checked arrival-square balustrade; never edit the world.
|
|
|
|
The existing polygon is followed exactly. Cardinal elbows close diagonal gaps;
|
|
garden-side elbows move outward and receive observed, grounded stone footings.
|
|
Every road clear cell, planted block and existing light remains protected.
|
|
"""
|
|
import argparse
|
|
from collections import Counter
|
|
import importlib.util
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
STAGE = ROOT / '.runtime/balustrade-stage04'
|
|
AIR = 'minecraft:air'
|
|
N = {'east': (1, 0), 'north': (0, -1), 'south': (0, 1), 'west': (-1, 0)}
|
|
|
|
|
|
def module(name, path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
value = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(value)
|
|
return value
|
|
|
|
|
|
survey = module('balustrade_survey', ROOT / 'scripts/foundation-survey.py')
|
|
geometry = module('balustrade_geometry', ROOT / 'scripts/foundation-study/geometry.py')
|
|
|
|
|
|
def compile_plan(before):
|
|
design = json.loads((ROOT / '.runtime/plaza-stage03/design-final.json').read_text())
|
|
geo = json.loads((ROOT / '.runtime/foundations-stage02/foundations-final.geometry.json').read_text())
|
|
cells = {(c['x'], c['z']): c for c in geo['cells']}
|
|
roads = {tuple(p) for r in geo['routes'] for p in r['clear_cells']}
|
|
outer = geometry.polygon_cells(design['outer_hex'])
|
|
edge = {p for p in outer if any((p[0]+dx, p[1]+dz) not in outer for dx, dz in N.values())}
|
|
gardens = set()
|
|
for bed in design['beds']:
|
|
gardens |= geometry.polygon_cells(bed['outline'])
|
|
gardens |= {tuple(p) for p in bed.get('additional_planting_columns', [])}
|
|
ordered = sorted(edge, key=lambda p: math.atan2(p[1]-9, p[0]))
|
|
chain, elbows = [], []
|
|
for a, b in zip(ordered, ordered[1:]+ordered[:1]):
|
|
chain.append(a)
|
|
dx, dz = b[0]-a[0], b[1]-a[1]
|
|
if max(abs(dx), abs(dz)) > 1:
|
|
raise ValueError('Boundary order contains a nonadjacent jump')
|
|
if dx and dz:
|
|
candidates = [(a[0], b[1]), (b[0], a[1])]
|
|
def score(p):
|
|
state = before.state(p[0], 96, p[1])
|
|
return (p in edge or p in chain, p in gardens or state != AIR, p not in outer, p)
|
|
# An opening remains open even when its diagonal connector lies in it.
|
|
p = min(candidates, key=score)
|
|
chain.append(p)
|
|
elbows.append(p)
|
|
if len(set(chain)) != len(chain):
|
|
raise ValueError('Cardinal perimeter is not a simple cycle')
|
|
selected = {p for p in chain if p not in roads}
|
|
for p in selected:
|
|
state = before.state(p[0], 96, p[1])
|
|
if state != AIR and not ('stone_brick_wall[' in state or
|
|
(p in edge and state == 'minecraft:smooth_sandstone_slab[type=bottom,waterlogged=false]')):
|
|
raise ValueError(f'Protected decoration intersects fence at {p}: {state}')
|
|
if before.state(p[0], 97, p[1]) != AIR:
|
|
raise ValueError(f'Protected decoration intersects handrail at {p}')
|
|
# Rotate at an opening, then split into uninterrupted fence runs.
|
|
cut = next(i for i, p in enumerate(chain) if p not in selected)
|
|
linear = chain[cut:]+chain[:cut]
|
|
runs, run = [], []
|
|
for p in linear:
|
|
if p in selected:
|
|
run.append(p)
|
|
elif run:
|
|
runs.append(run)
|
|
run = []
|
|
if run:
|
|
runs.append(run)
|
|
piers = set()
|
|
for run in runs:
|
|
segments = max(1, round((len(run)-1)/8))
|
|
piers |= {run[round(i*(len(run)-1)/segments)] for i in range(segments+1)}
|
|
piers |= {tuple(p) for p in design['outer_hex'] if tuple(p) in run}
|
|
desired, groups, footing_columns, replaced_rims = {}, {}, [], []
|
|
def put(x, y, z, state, group):
|
|
if (x, z) in roads:
|
|
raise ValueError(f'Protected road cell at {(x, z)}')
|
|
before.state(x, y, z)
|
|
desired[x, y, z] = 'minecraft:'+state
|
|
groups[x, y, z] = group
|
|
for x, z in sorted(selected):
|
|
if (x, z) not in outer:
|
|
# Stop at an observed full support; never assume air or bury plants.
|
|
y = 95
|
|
while before.state(x, y, z) == AIR:
|
|
y -= 1
|
|
support = before.state(x, y, z).split('[')[0].removeprefix('minecraft:')
|
|
if support not in survey.FULL or y > 95:
|
|
raise ValueError(f'No simple observed footing at {(x, y, z)}')
|
|
if y < 95:
|
|
footing_columns.append({'x': x, 'z': z, 'support_y': y})
|
|
if support == 'grass_block':
|
|
put(x, y, z, 'dirt', 'stable-buried-footing-soil')
|
|
for yy in range(y+1, 96):
|
|
put(x, yy, z, 'cut_sandstone' if yy == 95 else 'stone_bricks', 'grounded-elbow-footing')
|
|
if before.state(x, 96, z).startswith('minecraft:smooth_sandstone_slab'):
|
|
replaced_rims.append([x, z])
|
|
if (x, z) in piers:
|
|
state = 'cut_sandstone'
|
|
else:
|
|
connected = {}
|
|
for name, (dx, dz) in N.items():
|
|
neighbor = before.state(x+dx, 96, z+dz).split('[')[0].removeprefix('minecraft:')
|
|
connected[name] = (x+dx, z+dz) in selected or neighbor.endswith('_wall') or neighbor in survey.FULL
|
|
straight = (connected['east'] and connected['west'] and not connected['north'] and not connected['south']) or (connected['north'] and connected['south'] and not connected['east'] and not connected['west'])
|
|
props = {name: 'tall' if value else 'none' for name, value in connected.items()}
|
|
props |= {'up': 'false' if straight else 'true', 'waterlogged': 'false'}
|
|
state = 'stone_brick_wall['+','.join(f'{k}={v}' for k, v in sorted(props.items()))+']'
|
|
put(x, 96, z, state, 'sandstone-piers' if (x, z) in piers else 'connected-stone-balusters')
|
|
put(x, 97, z, 'smooth_sandstone_slab[type=bottom,waterlogged=false]', 'continuous-cream-handrail')
|
|
# Old uncapped road rails need reciprocal arms where the new fence joins them.
|
|
adjacent = {(x+dx, z+dz) for x, z in selected for dx, dz in N.values()} - selected
|
|
neighboring_rails = {p for p in adjacent if before.state(p[0], 96, p[1]).startswith('minecraft:stone_brick_wall[')}
|
|
for x, z in sorted(neighboring_rails):
|
|
if before.state(x, 97, z) != AIR:
|
|
raise ValueError('Neighboring rail has an unsupported cap configuration')
|
|
links = {}
|
|
for name, (dx, dz) in N.items():
|
|
p = x+dx, 96, z+dz
|
|
neighbor = desired.get(p, before.state(*p)).split('[')[0].removeprefix('minecraft:')
|
|
links[name] = neighbor.endswith('_wall') or neighbor in survey.FULL
|
|
straight = (links['east'] and links['west'] and not links['north'] and not links['south']) or (links['north'] and links['south'] and not links['east'] and not links['west'])
|
|
props = {name: 'low' if linked else 'none' for name, linked in links.items()}
|
|
props |= {'up': 'false' if straight else 'true', 'waterlogged': 'false'}
|
|
put(x, 96, z, 'stone_brick_wall['+','.join(f'{k}={v}' for k, v in sorted(props.items()))+']', 'reciprocal-road-rail-joins')
|
|
blocks = [dict(zip(('x', 'y', 'z'), p)) | {'block': state, 'expected': before.state(*p), 'group': groups[p]}
|
|
for p, state in sorted(desired.items()) if state != before.state(*p)]
|
|
meta = {'version': 1, 'scope': before.scope, 'floor_y': 95, 'handrail_top_y': 97.5,
|
|
'fence_columns': sorted(selected), 'pier_columns': sorted(piers), 'path_runs': runs,
|
|
'elbow_columns': sorted(set(elbows) & selected), 'footing_columns': footing_columns,
|
|
'neighboring_rail_columns': sorted(neighboring_rails),
|
|
'replaced_planter_rims': replaced_rims, 'protected_road_columns': sorted(roads),
|
|
'by_group': dict(Counter(b['group'] for b in blocks)), 'changed_blocks': len(blocks)}
|
|
# Reuse the independent garden and inherited-route auditor with the new obstacle mask.
|
|
garden_meta = json.loads((ROOT / '.runtime/plaza-stage03/plaza-polished.metadata.json').read_text())
|
|
garden_meta['unwalkable_columns'] = sorted({tuple(p) for p in garden_meta['unwalkable_columns']} | selected)
|
|
walk = json.loads((ROOT / '.runtime/plaza-stage03/plaza-polished.walk.json').read_text())
|
|
walk['points'] = [p for p in walk['points'] if (p['x'], p['z']) not in selected]
|
|
garden_meta['walk_samples'] = len(walk['points'])
|
|
return {'version': 1, 'scope': before.scope, 'blocks': blocks}, meta, garden_meta, walk
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--before', type=Path, default=STAGE / 'before-full.json.gz')
|
|
parser.add_argument('--output', type=Path, default=STAGE / 'balustrade.json')
|
|
args = parser.parse_args()
|
|
before = survey.load_snapshot(args.before)
|
|
plan, meta, garden_meta, walk = compile_plan(before)
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
for suffix, document in (('.json', plan), ('.metadata.json', meta), ('.garden-metadata.json', garden_meta), ('.walk.json', walk)):
|
|
args.output.with_suffix(suffix).write_text(json.dumps(document, separators=(',', ':'))+'\n')
|
|
print(json.dumps({k: meta[k] for k in ('changed_blocks', 'by_group', 'replaced_planter_rims')}
|
|
| {'fence_columns': len(meta['fence_columns']), 'piers': len(meta['pier_columns']),
|
|
'runs': [len(r) for r in meta['path_runs']], 'footings': len(meta['footing_columns'])}))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|