Files
minecraft-builder-mcp/scripts/build-shacraft-balustrade.py
T
Emil 89c53d7058 Expand material support and checkpoint the completed building toolkit
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.
2026-09-13 16:59:13 +03:00

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()