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.
295 lines
15 KiB
Python
295 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""Compile a reference-led arrival garden against observed, protected voxel states.
|
|
|
|
No world writes. Apply the resulting checked recipe with scripts/layout.py.
|
|
The brand bitmap is sampled into block coordinates, not used as a rendered fake.
|
|
"""
|
|
import argparse
|
|
from collections import Counter, deque
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
N2 = ((1, 0), (-1, 0), (0, 1), (0, -1))
|
|
N3 = ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1))
|
|
AIR = 'minecraft:air'
|
|
|
|
|
|
def module(name, path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
m = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(m)
|
|
return m
|
|
|
|
|
|
geometry = module('plaza_geometry', ROOT / 'scripts/foundation-study/geometry.py')
|
|
survey = module('plaza_survey', ROOT / 'scripts/foundation-survey.py')
|
|
assets = module('plaza_assets', ROOT / 'scripts/plaza-assets.py')
|
|
|
|
|
|
def distances(mask):
|
|
result = {p: 0 for p in mask if any((p[0]+dx, p[1]+dz) not in mask for dx, dz in N2)}
|
|
pending = deque(result)
|
|
while pending:
|
|
x, z = pending.popleft()
|
|
for dx, dz in N2:
|
|
p = x+dx, z+dz
|
|
if p in mask and p not in result:
|
|
result[p] = result[x, z]+1
|
|
pending.append(p)
|
|
return result
|
|
|
|
|
|
def brand_cells(path, width=25, height=35):
|
|
from PIL import Image
|
|
source = Image.open(path).convert('RGBA')
|
|
# Select the green artwork, excluding transparency and the pale antialias fringe.
|
|
mask = Image.new('L', source.size)
|
|
pixels = source.get_flattened_data() if hasattr(source, 'get_flattened_data') else source.getdata()
|
|
mask.putdata([255 if a > 100 and g > r*1.15 and g > b*1.15 else 0 for r, g, b, a in pixels])
|
|
box = mask.getbbox()
|
|
if not box:
|
|
raise ValueError('Brand image contains no green artwork')
|
|
cells = mask.crop(box).resize((width, height), Image.Resampling.BOX)
|
|
return {(x-width//2, z+9-height//2) for z in range(height) for x in range(width)
|
|
if cells.getpixel((x, z)) >= 115}
|
|
|
|
|
|
def compile_plan(design, before, previous, logo):
|
|
base_geometry = json.loads((ROOT / '.runtime/foundations-stage02/foundations-final.geometry.json').read_text())
|
|
cells = {(c['x'], c['z']): c for c in base_geometry['cells']}
|
|
outer = geometry.polygon_cells(design['outer_hex'])
|
|
floor = {p for p in outer if cells[p]['kind'] == 'full' and cells[p]['block_y'] == 95}
|
|
edge = distances(outer)
|
|
protected = {tuple(p) for route in base_geometry['routes'] for p in route['clear_cells']}
|
|
desired, groups = {}, {}
|
|
|
|
def put(x, y, z, state, group):
|
|
if not state.startswith('minecraft:'):
|
|
state = 'minecraft:'+state
|
|
before.state(x, y, z) # Require observed support and air, including canopy overhangs.
|
|
desired[x, y, z], groups[x, y, z] = state, group
|
|
|
|
def current(x, y, z):
|
|
return desired.get((x, y, z), before.state(x, y, z))
|
|
|
|
# Retire only still-matching zone01 survey marks, including its exterior
|
|
# stakes and number. Other districts and changed human blocks are preserved.
|
|
marker_groups = {'arrival-hex', 'spawn-medallion', 'spawn-monogram', 'label-01', 'wayfinding-01'}
|
|
markers = json.loads((ROOT/'.runtime/layout-study/markers-final.json').read_text())
|
|
for b in markers['blocks']:
|
|
if b.get('group') not in marker_groups:
|
|
continue
|
|
p = b['x'], b['y'], b['z']
|
|
try:
|
|
observed = before.state(*p)
|
|
except (KeyError, ValueError):
|
|
continue
|
|
if observed == b['block']:
|
|
put(*p, b['expected'], 'retire-zone01-survey')
|
|
|
|
# Preserve stair treads, the foundation footprint, and all neighboring districts.
|
|
for x, z in sorted(floor):
|
|
material = 'smooth_sandstone'
|
|
if edge[x, z] == 0:
|
|
material = 'cut_sandstone'
|
|
elif edge[x, z] == 1:
|
|
material = 'smooth_stone'
|
|
put(x, 95, z, material, 'cream-paving')
|
|
|
|
for radius, material in ((22, 'polished_andesite'), (24, 'smooth_stone'), (28, 'cut_sandstone')):
|
|
poly = [(round(radius*math.sin(i*math.pi/3)), 9-round(radius*math.cos(i*math.pi/3))) for i in range(6)]
|
|
ring = geometry.polygon_cells(poly)
|
|
for x, z in ring:
|
|
if any((x+dx, z+dz) not in ring for dx, dz in N2):
|
|
put(x, 95, z, material, 'hexagonal-paving-bands')
|
|
logo_mask = brand_cells(logo)
|
|
for x, z in logo_mask:
|
|
if (x, z) not in floor:
|
|
raise ValueError('Logo exceeds the paving')
|
|
put(x, 95, z, 'green_concrete', 'original-brand-inlay')
|
|
|
|
# Replace the former bulky survey-stage light piers. Their perimeter rails remain.
|
|
old_plan = json.loads((ROOT / '.runtime/foundations-stage02/foundations-final.json').read_text())
|
|
old_lamps = [(b['x'], b['z']) for b in old_plan['blocks'] if b.get('group') == 'lamps'
|
|
and ((b['x'], b['z']) in outer or (b['x'], b['z']) == (-5, 56))]
|
|
for x, z in old_lamps:
|
|
for y in range(96, 100):
|
|
put(x, y, z, 'air', 'replace-old-lamp-piers')
|
|
props = {}
|
|
for name, (dx, dz) in {'east': (1, 0), 'north': (0, -1), 'south': (0, 1), 'west': (-1, 0)}.items():
|
|
neighbor = before.state(x+dx, 96, z+dz)
|
|
props[name] = 'low' if any(n in neighbor for n in ('stone_brick_wall', 'chiseled_stone_bricks')) else 'none'
|
|
put(x, 96, z, 'stone_brick_wall['+','.join(f'{k}={v}' for k, v in sorted(props.items() | {'up': 'true', 'waterlogged': 'false'}.items()))+']', 'restored-perimeter-rail')
|
|
|
|
beds, beds_union, bench_aprons, fixture_records = [], set(), set(), []
|
|
flower_types = ('pink_tulip', 'white_tulip', 'oxeye_daisy', 'allium', 'azure_bluet')
|
|
for index, bed in enumerate(design['beds']):
|
|
mask = geometry.polygon_cells(bed['outline'])
|
|
mask |= {tuple(p) for p in bed.get('additional_planting_columns', [])}
|
|
if not mask <= floor or mask & protected:
|
|
raise ValueError('Garden intersects a protected road or non-flat foundation')
|
|
beds.append(mask)
|
|
beds_union |= mask
|
|
inset = distances(mask)
|
|
for x, z in sorted(mask):
|
|
put(x, 94, z, 'dirt', 'garden-soil')
|
|
put(x, 95, z, 'grass_block[snowy=false]', 'garden-soil')
|
|
if inset[x, z] == 0:
|
|
put(x, 95, z, 'smooth_sandstone', 'planter-rim')
|
|
put(x, 96, z, 'smooth_sandstone_slab[type=bottom,waterlogged=false]', 'planter-rim')
|
|
else:
|
|
# Drifts follow small clusters, rather than an alternating plant checkerboard.
|
|
patch = (x//3+2*(z//3)+index) % 7
|
|
if inset[x, z] == 1 and patch in (0, 1, 5):
|
|
put(x, 96, z, 'oak_leaves[distance=7,persistent=true,waterlogged=false]', 'low-evergreen-hedge')
|
|
elif (x*17+z*31) % 5 != 0:
|
|
flower = flower_types[patch % len(flower_types)]
|
|
put(x, 96, z, flower, 'flower-drifts')
|
|
|
|
tree = bed['tree']
|
|
put(tree['x'], 95, tree['z'], 'dirt', 'stable-tree-soil')
|
|
height = max(11, min(15, tree['height_above_floor']))
|
|
for (dx, y, dz), state in assets.conifer(height, seed=1337+index*71).items():
|
|
if 'leaves' in state and y < 4:
|
|
continue
|
|
put(tree['x']+dx, 96+y, tree['z']+dz, state, 'custom-conifers')
|
|
fixture_records.append({'type': 'conifer', 'x': tree['x'], 'z': tree['z'], 'height': height})
|
|
t = bed['small_topiary']
|
|
put(t['x'], 95, t['z'], 'dirt', 'stable-tree-soil')
|
|
for y in range(4):
|
|
put(t['x'], 96+y, t['z'], 'spruce_log[axis=y]', 'small-topiary')
|
|
for y, radius in ((2, 1), (3, 1), (4, 0)):
|
|
for dx in range(-radius, radius+1):
|
|
for dz in range(-radius, radius+1):
|
|
if dx*dx+dz*dz <= 2 and (dx or dz or y == 4):
|
|
put(t['x']+dx, 96+y, t['z']+dz, 'spruce_leaves[distance=7,persistent=true,waterlogged=false]', 'small-topiary')
|
|
fixture_records.append({'type': 'topiary', 'x': t['x'], 'z': t['z'], 'height': 5})
|
|
|
|
facing_vectors = {'north': (0, -1), 'east': (1, 0), 'south': (0, 1), 'west': (-1, 0)}
|
|
opposite = {'north': 'south', 'south': 'north', 'east': 'west', 'west': 'east'}
|
|
for bed, mask in zip(design['beds'], beds):
|
|
b = bed['bench']
|
|
cx, cz = b['center']
|
|
facing = opposite[b['back_faces']]
|
|
dx, dz = facing_vectors[facing]
|
|
# Open a level, three-block seat approach through the planter rim.
|
|
for offset in (-1, 0, 1):
|
|
for step in range(1, 8):
|
|
x, z = cx+dx*step-dz*offset, cz+dz*step+dx*offset
|
|
if (x, z) not in floor:
|
|
raise ValueError('Bench approach exceeds the plaza')
|
|
for y in (96, 97):
|
|
if '_log[' in current(x, y, z):
|
|
raise ValueError('Bench approach intersects a tree')
|
|
put(x, y, z, 'air', 'bench-access')
|
|
put(x, 95, z, 'smooth_sandstone', 'bench-access')
|
|
bench_aprons.add((x, z))
|
|
if (x, z) not in mask:
|
|
break
|
|
for (ox, y, oz), state in assets.bench(b['length'], facing).items():
|
|
x, z = cx+ox, cz+oz
|
|
if (x, z) not in floor or (x, z) in protected:
|
|
raise ValueError('Bench exceeds its clear garden bay')
|
|
for yy in (96, 97):
|
|
if '_log[' in current(x, yy, z):
|
|
raise ValueError('Bench intersects a tree root')
|
|
put(x, yy, z, 'air', 'bench-access')
|
|
put(x, 95, z, 'smooth_sandstone', 'bench-foundation')
|
|
put(x, 96+y, z, state, 'garden-benches')
|
|
fixture_records.append({'type': 'bench', 'x': cx, 'z': cz, 'facing': facing, 'seats': 3})
|
|
|
|
lamps = [(v['x'], v['z']) for v in design['lighting']['lamps']]
|
|
for x, z in lamps:
|
|
c = cells.get((x, z), {})
|
|
if (x, z) in protected or c.get('kind') != 'full' or c.get('block_y') != 95:
|
|
raise ValueError('Lamp base intrudes into an approach')
|
|
put(x, 95, z, 'chiseled_stone_bricks', 'lamp-footing')
|
|
for (dx, y, dz), state in assets.lamp(6).items():
|
|
put(x+dx, 96+y, z+dz, state, 'copper-garden-lamps')
|
|
fixture_records.append({'type': 'lamp', 'x': x, 'z': z, 'lantern_y': 100})
|
|
|
|
# Leaves use their stable distance to the composed logs, including touching hedges.
|
|
leaf_positions = {p for p, state in desired.items() if '_leaves[' in state}
|
|
logs = {p for p, state in desired.items() if '_log[' in state}
|
|
leaf_dist, pending = {}, deque()
|
|
for x, y, z in logs:
|
|
for dx, dy, dz in N3:
|
|
p = x+dx, y+dy, z+dz
|
|
if p in leaf_positions:
|
|
leaf_dist[p] = 1
|
|
pending.append(p)
|
|
while pending:
|
|
x, y, z = pending.popleft()
|
|
if leaf_dist[x, y, z] >= 6:
|
|
continue
|
|
for dx, dy, dz in N3:
|
|
p = x+dx, y+dy, z+dz
|
|
if p in leaf_positions and p not in leaf_dist:
|
|
leaf_dist[p] = leaf_dist[x, y, z]+1
|
|
pending.append(p)
|
|
for p in leaf_positions:
|
|
material = desired[p].split('[', 1)[0]
|
|
desired[p] = f'{material}[distance={leaf_dist.get(p, 7)},persistent=true,waterlogged=false]'
|
|
|
|
# Every floor column with a low obstacle is excluded from the walking network.
|
|
blocked = set()
|
|
for x, z in floor:
|
|
if current(x, 96, z) != AIR or current(x, 97, z) != AIR:
|
|
blocked.add((x, z))
|
|
blocked |= beds_union - bench_aprons
|
|
if blocked & protected:
|
|
raise ValueError(f'Decoration obstructs protected road cells: {sorted(blocked & protected)[:8]}')
|
|
for x, z in blocked:
|
|
if abs(x) <= 6 and (x, z) not in beds_union:
|
|
# Existing outer balustrades are handled by the previous navigation mask.
|
|
if any(groups.get((x, y, z), '') not in ('restored-perimeter-rail', '') for y in (96, 97)):
|
|
raise ValueError('Central sightline/walking axis was obstructed')
|
|
|
|
blocks = []
|
|
for p, state in sorted(desired.items()):
|
|
observed = before.state(*p)
|
|
try:
|
|
prior = previous.state(*p)
|
|
except (KeyError, ValueError):
|
|
if p[1] <= 114:
|
|
raise
|
|
prior = AIR
|
|
if observed != prior:
|
|
raise ValueError(f'Unexpected manual edit preserved at {p}')
|
|
if state != observed:
|
|
blocks.append(dict(zip(('x', 'y', 'z'), p)) | {'block': state, 'expected': observed, 'group': groups[p]})
|
|
walk = [{'x': x, 'z': z, 'standing_y': 96} for x, z in sorted(floor-blocked)]
|
|
meta = {'scope': before.scope, 'district': '01', 'changed_blocks': len(blocks), 'planters': len(beds),
|
|
'planter_columns': len(beds_union), 'trees': 6, 'topiary': 6, 'benches': 6, 'new_lamps': len(lamps),
|
|
'old_lamps_replaced': len(old_lamps), 'logo_blocks': len(logo_mask), 'walk_samples': len(walk),
|
|
'unwalkable_columns': sorted(blocked), 'bench_access_columns': sorted(bench_aprons),
|
|
'fixtures': fixture_records, 'by_material': dict(Counter(b['block'] for b in blocks)),
|
|
'by_group': dict(Counter(b['group'] for b in blocks)), 'source': 'Observed after-foundation baseline; approved reference sheets 03 and 11'}
|
|
return {'version': 1, 'scope': before.scope, 'blocks': blocks}, meta, {'scope': before.scope, 'points': walk}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--design', type=Path, default=ROOT/'.runtime/plaza-stage03/design-final.json')
|
|
parser.add_argument('--before', type=Path, default=ROOT/'.runtime/plaza-stage03/before.json.gz')
|
|
parser.add_argument('--output', type=Path, default=ROOT/'.runtime/plaza-stage03/plaza.json')
|
|
parser.add_argument('--logo', type=Path, default=Path('/home/emil/Desktop/Shacraft-Lobby-References/brand/logo-180.png'))
|
|
args = parser.parse_args()
|
|
before = survey.load_snapshot(args.before)
|
|
previous = survey.load_snapshot(ROOT/'.runtime/foundations-stage02/after.json.gz')
|
|
design = json.loads(args.design.read_text())
|
|
plan, meta, walk = compile_plan(design, before, previous, args.logo)
|
|
meta['source_logo_sha256'] = hashlib.sha256(args.logo.read_bytes()).hexdigest()
|
|
args.output.write_text(json.dumps(plan, separators=(',', ':'))+'\n')
|
|
args.output.with_suffix('.metadata.json').write_text(json.dumps(meta, indent=2)+'\n')
|
|
args.output.with_suffix('.walk.json').write_text(json.dumps(walk, separators=(',', ':'))+'\n')
|
|
print(json.dumps({k: v for k, v in meta.items() if k not in ('unwalkable_columns', 'bench_access_columns', 'fixtures', 'by_material')}))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|