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.
This commit is contained in:
Emil
2026-09-13 16:59:13 +03:00
parent 0290559abf
commit 89c53d7058
179 changed files with 37719 additions and 162 deletions
+171
View File
@@ -0,0 +1,171 @@
#!/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()
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""Compile the reviewed foundations and streets into a checked, reversible block plan.
No server writes here. A full live voxel survey is required, and differences from
the known natural world + previous marker receipts are protected, not adopted.
Apply the resulting JSON with scripts/layout.py.
"""
import argparse
from collections import Counter, deque
import importlib.util
import json
import math
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
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
geometry=module('foundation_geometry',ROOT/'scripts/foundation-study/geometry.py')
survey=module('foundation_survey',ROOT/'scripts/foundation-survey.py')
N=((1,0),(-1,0),(0,1),(0,-1))
def distances(mask):
dist={p:0 for p in mask if any((p[0]+dx,p[1]+dz) not in mask for dx,dz in N)}
q=deque(dist)
while q:
x,z=q.popleft()
for dx,dz in N:
p=(x+dx,z+dz)
if p in mask and p not in dist:dist[p]=dist[(x,z)]+1;q.append(p)
return dist
def compile_plan(design,layout,before,original,marker_documents):
scope=before.document['scope'];geo=geometry.build_geometry(design,layout);cells=geo['cells']
mask=set(cells);edge=distances(mask);desired={};groups={}
known={}
for doc in marker_documents:
for b in doc['blocks']:known[(b['x'],b['y'],b['z'])]=b['block']
def natural(x,z):
i=(z-original['min_z'])*original['width']+x-original['min_x']
return original['surface_y'][i],original['palette'][original['material_index'][i]]
def predicted(x,y,z):
if (x,y,z) in known:return known[(x,y,z)]
h,material=natural(x,z)
if y>h:return 'minecraft:air'
if material=='minecraft:water':raise ValueError('Water column outside foundation scope')
if y==h:return material+'[snowy=false]' if material=='minecraft:grass_block' else material
return 'minecraft:dirt' if material=='minecraft:grass_block' and y>=h-3 else 'minecraft:stone'
def put(x,y,z,block,group):
if not block.startswith('minecraft:'):block='minecraft:'+block
desired[(x,y,z)]=block;groups[(x,y,z)]=group
# Remove only old survey blocks within this stage's exact footprint + 1-column
# cleanup margin. Outside district markers remain unchanged.
cleanup=mask|{(x+dx,z+dz) for x,z in mask for dx,dz in N}
for (x,y,z),state in known.items():
if (x,z) not in cleanup:continue
try:before.state(x,y,z)
except (KeyError,ValueError):continue # Only the optional cleanup margin may extend beyond the survey.
h,m=natural(x,z)
if y>h:new='minecraft:air'
elif y==h:new=m+'[snowy=false]' if m=='minecraft:grass_block' else m
else:continue
put(x,y,z,new,'remove-obsolete-survey')
road_clear={tuple(p) for r in geo['routes'] for p in r['clear_cells']}
road_buffer=road_clear|{(x+dx,z+dz) for x,z in road_clear for dx,dz in N}
# Exact solid footings and two structural courses beneath every walking deck.
for (x,z),c in cells.items():
y=c['block_y'];h,_=natural(x,z);d=edge[(x,z)];group=c['group']
bottom=min(h-1,y-2) if d<=1 else min(h,y-2)
for yy in range(bottom,y):
block='stone'
if d==0:
block='deepslate_bricks' if yy<=h+1 else 'stone_bricks'
if yy==y-1:block='polished_andesite'
if (x+z)%12 in (0,1) and yy>h+1:block='smooth_sandstone'
elif d==1 and yy==y-1:block='stone_bricks'
put(x,yy,z,block,group+'-foundation')
# Clear all natural overburden and former stakes above the finished floor.
for yy in range(y+1,max(h+2,y+4)+1):put(x,yy,z,'air',group+'-clearance')
if c['kind']=='stairs':
paving=f"stone_brick_stairs[facing={c['facing']},half=bottom,shape=straight,waterlogged=false]"
elif not c['clear'] or d==0:paving='polished_andesite'
elif d==1:paving='stone_bricks'
elif d==2:paving='smooth_sandstone'
elif group=='clock-station':
# Quiet structural floor; regular narrow foundation setting-out bands.
paving='stone_bricks' if x%16==0 or z%16==0 else 'smooth_stone'
elif group in ('arrival-hex','station-forecourt'):
paving='smooth_sandstone'
if x%12==0 or z%12==0:paving='smooth_stone'
else:
paving='smooth_stone'
if c['kind']=='full' and ((x if 'radial' in group or 'ring-' in group else z)%8==0):paving='stone_bricks'
put(x,y,z,paving,group+'-paving')
# Green inset medallion. All tesserae are flush with the arrival paving.
hexagon=[(round(16*math.sin(math.pi/3*i)),round(9-16*math.cos(math.pi/3*i))) for i in range(6)]
medallion=geometry.polygon_cells(hexagon);md=distances(medallion)
for x,z in medallion:
put(x,95,z,'smooth_quartz' if md[(x,z)]<=1 else 'green_concrete','arrival-medallion')
glyph=['01110','11000','11000','01110','00011','00011','01110']
for row,bits in enumerate(glyph):
for col,on in enumerate(bits):
if on=='1':
for dx in (0,1):
for dz in (0,1):put(-5+col*2+dx,95,2+row*2+dz,'smooth_quartz','arrival-medallion')
# Keep the future tower and pavilion footing outlines readable in the deck.
for f in layout['features']:
if f['id'] not in ('station-clock-base','station-pavilion--63','station-pavilion-29'):continue
vertices=f['points'];outline=geometry.polygon_cells(vertices)
for x,z in outline:
if any((x+dx,z+dz) not in outline for dx,dz in N):put(x,98,z,'polished_andesite','station-structural-bands')
# A shallow blind arcade breaks up the tall western station retaining wall.
# Each opening is one block deep, with an intact solid backing and lintel.
for center in (-132,-124,-116,-108):
for offset in range(-2,3):
z=center+offset;h,_=natural(-74,z);top=94-abs(offset)
for y in range(max(h+2,85),top+1):
put(-74,y,z,'air','station-west-blind-arcade')
put(-73,y,z,'deepslate_bricks','station-west-blind-arcade')
if top>=h+2:put(-74,top+1,z,'smooth_sandstone','station-west-arch-stones')
for z0 in (-138,-128,-120,-112,-104,-99):
for z in (z0,z0+1):
h,_=natural(-75,z)
for y in range(h-1,98):put(-75,y,z,'deepslate_bricks' if y<h+2 else 'stone_bricks','station-west-pilasters')
put(-75,98,z,'smooth_sandstone','station-west-pilasters')
# Guard exposed edges while reserving every planned road opening at full width.
rail={}
for (x,z),c in cells.items():
if edge[(x,z)]!=0:continue
if c['clear'] and (x,z) in road_buffer:continue
outside=[(x+dx,z+dz) for dx,dz in N if (x+dx,z+dz) not in cells]
drop=max([c['block_y']-natural(*p)[0] for p in outside] or [0])
if drop>=3 or c['group']=='station-entrance-stair':rail[(x,z)]=c['block_y']+1
for (x,z),y in rail.items():
props={name:('low' if (x+dx,z+dz) in rail and abs(rail[(x+dx,z+dz)]-y)<=1 else 'none')
for name,(dx,dz) in {'east':(1,0),'north':(0,-1),'south':(0,1),'west':(-1,0)}.items()}
straight=(props['east']==props['west']=='low' and props['north']==props['south']=='none') or (props['north']==props['south']=='low' and props['east']==props['west']=='none')
up='false' if straight and (x+z)%8 else 'true'
state=f"stone_brick_wall[east={props['east']},north={props['north']},south={props['south']},up={up},waterlogged=false,west={props['west']}]"
put(x,y,z,state,'edge-balustrades')
# Roads meet the reserved bridge decks exactly. A temporary end balustrade
# prevents a finished street from leading straight into an unbuilt span.
bridge_gates=[]
for ident in ('east-radial-local','ring-northeast-local'):
r=next(r for r in geo['routes'] if r['id']==ident)
end_x,end_z=r['centerline'][-1];deck=cells[(end_x,end_z)]['block_y']
zs=sorted(z for x,z in r['corridor'] if x==end_x)
for z in zs:
x=end_x+1;h,_=natural(x,z)
for y in range(min(h,deck-1),deck+1):put(x,y,z,'stone_bricks','temporary-bridge-threshold')
north='low' if z-1 in zs else 'none';south='low' if z+1 in zs else 'none'
put(x,deck+1,z,f'stone_brick_wall[east=none,north={north},south={south},up=true,waterlogged=false,west=none]','temporary-bridge-gates')
bridge_gates.append({'x':end_x+1.5,'y':deck+4,'z':end_z+.5,'deck_y':deck})
# Lit piers are part of the stone edge, never obstacles in the clear road lane.
candidates=[p for p in rail if cells[p]['kind']=='full']
chosen=[]
landmarks=[(0,-37),(38,-14),(42,30),(0,56),(-42,30),(-38,-14),
(-36,-61),(27,-61),(-25,-83),(13,-83),(-74,-139),(40,-139),(-74,-98),(40,-98)]
for target in landmarks:
options=sorted(candidates,key=lambda p:(p[0]-target[0])**2+(p[1]-target[1])**2)
if options and math.dist(options[0],target)<12 and all(math.dist(options[0],p)>8 for p in chosen):chosen.append(options[0])
for point in sorted(candidates,key=lambda p:(p[1],p[0])):
if all(math.dist(point,p)>19 for p in chosen):chosen.append(point)
for x,z in chosen:
y=cells[(x,z)]['block_y']
put(x,y+1,z,'chiseled_stone_bricks','lamp-piers');put(x,y+2,z,'stone_bricks','lamp-piers')
put(x,y+3,z,'smooth_stone_slab[type=double,waterlogged=false]','lamp-piers')
put(x,y+4,z,'lantern[hanging=false,waterlogged=false]','lamps')
# Preserve any unexpected human edits, including underground blocks. We do
# not silently rebase onto arbitrary newly observed content.
blocks=[];mismatches=[]
for (x,y,z),block in sorted(desired.items()):
actual=before.state(x,y,z);expected=predicted(x,y,z)
if actual!=expected:
mismatches.append({'x':x,'y':y,'z':z,'expected':expected,'actual':actual})
continue
if block!=actual:blocks.append({'x':x,'y':y,'z':z,'block':block,'expected':actual,'group':groups[(x,y,z)]})
if mismatches:raise ValueError(f'Unexpected live edits preserved: {len(mismatches)}, first {mismatches[:5]}')
# Sample both treads of every stair and every usable full floor column. Rails
# and lamp piers are excluded; actual after-survey tests their surrounding lanes.
walk=[]
for (x,z),c in cells.items():
if (x,z) in rail or not c['clear']:continue
if c['kind']=='stairs':
for sub in (.25,.75):
sx,sz=(sub,.5) if c['facing'] in ('east','west') else (.5,sub)
walk.append({'x':x,'z':z,'standing_y':geometry.tread_height(c,sx,sz),'sub_x':sx,'sub_z':sz})
else:walk.append({'x':x,'z':z,'standing_y':c['block_y']+1})
metadata={'scope':scope,'geometry_checks':geo['checks'],'changed_blocks':len(blocks),'walk_samples':len(walk),
'lamps':len(chosen),'rail_columns':len(rail),'by_material':dict(Counter(b['block'] for b in blocks)),
'temporary_bridge_gates':bridge_gates,
'source_snapshot_finished_at':before.document['finished_at'],'source_snapshot_atomic':False,
'note':'Only zones01/02 foundations and local access roads; other districts remain marked reservations.'}
return {'version':1,'scope':scope,'blocks':blocks},metadata,{'scope':scope,'points':walk},geo
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--design',type=Path,required=True);p.add_argument('--snapshot',type=Path,required=True)
p.add_argument('--output',type=Path,required=True);a=p.parse_args()
before=survey.load_snapshot(a.snapshot)
design=json.loads(a.design.read_text());layout=json.loads((ROOT/'examples/layout/shacraft-lobby-layout.json').read_text())
original=json.loads((ROOT/'.runtime/server/plugins/ShacraftTerrain/maps/layout-before.json').read_text())
markers=[json.loads((ROOT/name).read_text()) for name in ('.runtime/layout-study/markers-final.json','.runtime/layout-study/access-blocks.json')]
plan,meta,walk,geo=compile_plan(design,layout,before,original,markers)
a.output.parent.mkdir(parents=True,exist_ok=True)
a.output.write_text(json.dumps(plan,separators=(',',':'))+'\n')
a.output.with_suffix('.metadata.json').write_text(json.dumps(meta,indent=2)+'\n')
a.output.with_suffix('.walk.json').write_text(json.dumps(walk,separators=(',',':'))+'\n')
a.output.with_suffix('.geometry.json').write_text(json.dumps(geometry.serializable(geo),separators=(',',':'))+'\n')
print(json.dumps(meta))
if __name__=='__main__':main()
+294
View File
@@ -0,0 +1,294 @@
#!/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()
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Compile the two-floor clock station against observed, unchanged foundation voxels.
No live writes: apply the resulting recipe using scripts/layout.py after QA.
"""
import argparse
import importlib.util
import json
from collections import Counter
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
STAGE=ROOT/'.runtime/station-stage07'
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('station_survey',ROOT/'scripts/foundation-survey.py')
def canonical(state):
if '[' not in state:return state
name,raw=state[:-1].split('[',1)
return name+'['+','.join(sorted(raw.split(',')))+']'
def compile_station(before):
layout=json.loads((ROOT/'docs/references/zone02-interior-v1/layout.json').read_text())
geometry=json.loads((ROOT/'.runtime/foundations-stage02/foundations-final.geometry.json').read_text())
foundation=survey.load_snapshot(ROOT/'.runtime/foundations-stage02/after.json.gz')
foot={(c['x'],c['z']) for c in geometry['cells'] if c['group']=='clock-station'}
allowed=set(json.loads((STAGE/'context-public.json').read_text())['supported_materials'])
exterior=module('station_exterior',ROOT/'scripts/station-exterior.py')
interior=module('station_interior',ROOT/'scripts/station-interior.py')
states,groups,ext=exterior.compile_exterior(foot,layout)
decorations,labels,intmeta=interior.compile_interior(foot,layout)
changes=Counter()
for p,v in decorations.items():
if p in states and states[p]!=v:changes[(groups[p],labels[p])]+=1
states.update(decorations);groups.update(labels)
rows=[];conflicts=[]
for p,value in sorted(states.items()):
state=canonical(value)
if state.split('[')[0] not in allowed:raise ValueError(f'Material unavailable: {state}')
observed=before.state(*p)
if observed==state:continue
if observed not in survey.AIR:
try: prior=foundation.state(*p)
except KeyError:prior=None
if observed!=prior:conflicts.append({'at':p,'current':observed,'foundation_stage':prior})
rows.append(dict(zip(('x','y','z'),p))|{'block':state,'expected':observed,'group':groups[p]})
if conflicts:raise ValueError(f'Unexpected existing changes preserved: {conflicts[:12]} ({len(conflicts)} total)')
recipe={'version':1,'scope':before.scope,'blocks':rows}
desired={(b['x'],b['y'],b['z']):b['block'] for b in rows}
def state(x,y,z):return desired.get((x,y,z),before.state(x,y,z))
inner=exterior._erode(foot,2)
floors=[]
for feet,name in [(99,'vestibule'),(113,'smash')]:
candidates=set(inner)
if feet==99:candidates|={(x,z) for x in range(-9,-2) for z in range(-85,-82)}
clear=sorted(p for p in candidates if all(state(p[0],y,p[1]) in survey.AIR for y in range(feet,feet+4)))
floors.append({'id':name,'standing_y':feet,'source':{'x':-6,'z':-120},'clear_columns':clear,'min_headroom':4})
def region(name,x0,x1,y0,y1,z0,z1):
return {'id':name,'min':{'x':x0,'y':y0,'z':z0},'max':{'x':x1,'y':y1,'z':z1}}
clear_regions=[region('entrance',-9,-3,99,102,-85,-83)]
for feet in [99,113]:
clear_regions += [region(f'cabin-{feet}',-8,-4,feet,feet+3,-122,-118),
region(f'lift-door-{feet}',-8,-4,feet,feet+3,-117,-116)]
clear_regions += [region('ground-entry-aisle',-9,-3,99,102,-115,-86),
region('smash-bay-front',-46,11,113,116,-135,-129)]
metadata={'scope':before.scope,'exterior':ext,'interior':intmeta,'public_floors':floors,
'clear_regions':clear_regions,'footprint':sorted(foot),
'containment':{'bounds':{'min':{'x':-79,'y':94,'z':-150},'max':{'x':45,'y':163,'z':-76}},
'authorized_caps':[region('ground-entrance-audit-cap',-9,-3,99,110,-84,-84)],
'forbidden_y_at_or_above':126,
'seeds':[{'id':'vestibule-and-cabin','x':-6,'y':99,'z':-120,'min_y':99,'max_y':110},
{'id':'smash-and-cabin','x':-6,'y':113,'z':-120,'min_y':113,'max_y':123}]},
'compile_summary':{'written_blocks':len(rows),'intended_states':len(states),'materials':dict(Counter(b['block'].split('[')[0] for b in rows)),
'layer_overrides':[{'exterior':a,'interior':b,'count':n} for (a,b),n in changes.items()]}}
return recipe,metadata
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--before',type=Path,default=STAGE/'before.json.gz')
parser.add_argument('--output',type=Path,default=STAGE/'compiled')
args=parser.parse_args();recipe,metadata=compile_station(survey.load_snapshot(args.before))
args.output.mkdir(parents=True,exist_ok=True)
for name,data in [('station.json',recipe),('station.metadata.json',metadata)]:
(args.output/name).write_text(json.dumps(data,separators=(',',':'))+'\n')
print(json.dumps(metadata['compile_summary']))
if __name__=='__main__':main()
+3
View File
@@ -22,6 +22,9 @@ def main():
except (OSError, ValueError):
raise SystemExit('Cannot read a valid camera token/port from the private Paper config.')
environment = dict(os.environ, MCB_CAMERA_TOKEN=token, MCB_CAMERA_PORT=str(port))
auto = re.search(r'^camera-auto-connect:[ \t]*[\'\"]?(127\.0\.0\.1:[0-9]{1,5})[\'\"]?[ \t]*$', data, re.M)
if auto:
environment['MCB_CAMERA_AUTO_CONNECT'] = auto.group(1)
os.execvpe(sys.argv[1], sys.argv[1:], environment)
if __name__ == '__main__':
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Compile audited access refinements against the original survey plus placed markers."""
import argparse
import importlib.util
import json
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
spec=importlib.util.spec_from_file_location('mark',ROOT/'scripts/mark-shacraft-layout.py')
mark=importlib.util.module_from_spec(spec);spec.loader.exec_module(mark)
def main():
p=argparse.ArgumentParser();p.add_argument('--before',type=Path,required=True)
p.add_argument('--base',type=Path,required=True);p.add_argument('--study',type=Path,required=True)
p.add_argument('--access',type=Path,required=True);p.add_argument('--output',type=Path,required=True)
a=p.parse_args();before=json.loads(a.before.read_text());base=json.loads(a.base.read_text())
study=json.loads(a.study.read_text());access=json.loads(a.access.read_text());desired={}
old={(b['x'],b['y'],b['z']):b['block'] for b in base['blocks']}
def ground(x,z):
i=(z-before['min_z'])*before['width']+x-before['min_x']
return before['surface_y'][i],before['palette'][before['material_index'][i]]
def put(x,z,block,group,y=None):
x,z=round(x),round(z);h,material=ground(x,z);y=max(h,51) if y is None else y
if y<h:raise ValueError('Refusing subsurface edit')
if material=='minecraft:grass_block':material+='[snowy=false]'
key=(x,y,z);expected=old.get(key,material if y==h else 'minecraft:air')
if expected=='minecraft:water':raise ValueError('Refusing water edit')
if expected==block:return
desired[key]={'x':x,'y':y,'z':z,'block':block,'expected':expected,'group':group}
labels=[]
for route in access['routes']:
pts=route['points'];color='minecraft:white_concrete'
for side in (-1,1):
for x,z in mark.raster_line(mark.offset(pts,side),.6):put(x,z,color,route['id'])
if route['role']=='stairs':
for i in range(0,len(pts),6):
aa=mark.offset(pts,-1)[i];bb=mark.offset(pts,1)[i]
for x,z in mark.raster_line([aa,bb],.6):put(x,z,'minecraft:blue_concrete',route['id'])
for label in route.get('labels',[]):
x,z=label['point'];h,_=ground(x,z)
labels.append({'x':x+.5,'y':h+5,'z':z+.5,'text':label['text']})
for route in study['routes']:
if route['role']!='bridge':continue
pts=route['points'];deck=route['deck_y']
for side in (-1,1):
for end in (0,-1):
x,z=map(round,mark.offset(pts,side*(route['width']-1)/2)[end]);h,_=ground(x,z)
for y in range(max(50,h+1),deck+1):put(x,z,'minecraft:white_concrete',route['id']+'-height-stakes',y)
for abutment in route.get('abutments',[]):
if abutment['future_stairs']:
x,z=abutment['point'];h,_=ground(x,z)
labels.append({'x':x+.5,'y':deck+5,'z':z+.5,
'text':f"FUTURE STAIRS / +{deck-h}m\nDECK Y{deck}"})
# The raised pier reservations meet a future upper terminal level.
for z in (-158,-132,-106):labels.append({'x':161.5,'y':83,'z':z+.5,'text':'PIER DECK Y79 / FUTURE ACCESS'})
data={'version':1,'scope':base['scope'],'blocks':sorted(desired.values(),key=lambda b:(b['z'],b['x'],b['y']))}
a.output.write_text(json.dumps(data,separators=(',',':'))+'\n')
a.output.with_suffix('.labels.json').write_text(json.dumps(labels,indent=2)+'\n')
print(json.dumps({'access_blocks':len(desired),'labels':len(labels)}))
if __name__=='__main__':main()
+5 -1
View File
@@ -4,6 +4,7 @@ import argparse
import hashlib
import json
import os
import re
from pathlib import Path
import shutil
import subprocess
@@ -40,8 +41,11 @@ def prepare():
if __name__=='__main__':
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('--run',action='store_true')
ap.add_argument('--heap',default='2G',help='Maximum Java heap, e.g. 4G for the large lobby')
ap.add_argument('--accept-eula',action='store_true',help='Explicitly accept https://www.minecraft.net/eula before starting this private test server')
args=ap.parse_args()
if not re.fullmatch(r'[1-9][0-9]*[MG]', args.heap):
ap.error('--heap must be a positive integer followed by M or G')
server=prepare()
if args.accept_eula: (server/'eula.txt').write_text('# Accepted explicitly by the operator for this development server.\neula=true\n')
if args.run:
@@ -49,4 +53,4 @@ if __name__=='__main__':
raise SystemExit('Minecraft EULA acceptance required: https://www.minecraft.net/eula ; review then rerun with --accept-eula if you agree.')
java=Path(os.environ.get('MCB_JAVA_HOME',str(CACHE/'jdk-25.0.2')))/'bin/java'
if not java.exists(): raise SystemExit('Run python3 scripts/bootstrap-tools.py first')
raise SystemExit(subprocess.call([str(java),'-Dterminal.jline=false','-Dterminal.ansi=false','-Xms512M','-Xmx2G','-jar','paper.jar','--nogui'],cwd=server))
raise SystemExit(subprocess.call([str(java),'-Dterminal.jline=false','-Dterminal.ansi=false','-Xms512M','-Xmx'+args.heap,'-jar','paper.jar','--nogui'],cwd=server))
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Compile small arrival details and a separately reversible invisible enclosure.
Reads observed states only; apply the two recipes through scripts/layout.py.
The normal-player boundary has a full floor, side membrane and transparent roof.
Existing full stone cells can form the membrane; partial collision shapes cannot.
"""
import argparse
from collections import Counter
import importlib.util
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
STAGE = ROOT / '.runtime/zone01-stage05'
AIR = 'minecraft:air'
BARRIER = 'minecraft:barrier[waterlogged=false]'
N = ((1, 0), (-1, 0), (0, 1), (0, -1))
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('zone01_survey', ROOT / 'scripts/foundation-survey.py')
geometry = module('zone01_geometry', ROOT / 'scripts/foundation-study/geometry.py')
FULL = survey.FULL | {'waxed_oxidized_cut_copper', 'barrier'}
def full(state):
return state.split('[')[0].removeprefix('minecraft:') in FULL
def compile_plans(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())
rails = json.loads((ROOT / '.runtime/balustrade-stage04/balustrade-final.metadata.json').read_text())
outer = geometry.polygon_cells(design['outer_hex'])
roads = {tuple(p) for r in geo['routes'] for p in r['clear_cells']}
details, groups = {}, {}
def decorate(x, y, z, state, group):
at = x, y, z
old = before.state(*at)
if y >= 96 and (old != AIR or (x, z) in roads):
raise ValueError(f'Furniture touches existing decoration or an approach at {at}')
if y == 95 and not full(old):
raise ValueError(f'Inlay would change a stair or unsupported floor at {at}')
details[at] = 'minecraft:'+state
groups[at] = group
urns = [(-10, 35), (10, 35)]
for cx, cz in urns:
decorate(cx, 96, cz, 'cut_sandstone', 'urn-pedestals')
for dx, dz in N:
decorate(cx+dx, 96, cz+dz, 'smooth_sandstone_slab[type=bottom,waterlogged=false]', 'urn-plinths')
for dx in range(-1, 2):
for dz in range(-1, 2):
if dx == dz == 0:
bowl = 'dirt'
elif dx and dz:
bowl = 'cut_sandstone'
else:
facing = 'east' if dx == 1 else 'west' if dx == -1 else 'south' if dz == 1 else 'north'
bowl = f'smooth_sandstone_stairs[facing={facing},half=bottom,shape=straight,waterlogged=false]'
decorate(cx+dx, 97, cz+dz, bowl, 'urn-bowls')
decorate(cx, 98, cz, 'white_tulip', 'urn-flowers')
# A low freestanding enamel nameboard; its text display is a separate receipt.
decorate(-8, 96, 43, 'cut_sandstone', 'welcome-pedestal')
for x in range(-9, -6):
decorate(x, 97, 43, 'green_concrete', 'welcome-nameboard')
decorate(x, 98, 43, 'waxed_oxidized_cut_copper_slab[type=bottom,waterlogged=false]', 'welcome-coping')
thresholds = {
'north': [(x, z) for x in range(-4, 5) for z in (-31, -30)],
'south': [(x, z) for x in range(-4, 5) for z in (50, 51)],
'east': [(x, z) for x in (38, 39) for z in range(13, 20)],
'west': [(x, z) for x in (-40, -39) for z in range(16, 21)],
'northwest': [(x, z) for x in range(-36, -31) for z in range(-16, -9)
if (x, z) in roads and x-z in (-21, -20)],
}
for name, columns in thresholds.items():
for x, z in columns:
decorate(x, 95, z, 'smooth_stone', 'flush-threshold-'+name)
# Restrained green/brass corner accents stay at the exact paving height.
for x, z in (min(columns), max(columns)):
decorate(x, 95, z, 'waxed_oxidized_cut_copper', 'threshold-copper-insets')
# Solid joint piers stop a partial-shaped road rail from carrying a folded
# membrane through a whole garden-side balustrade and its connected plants.
for x, z in ((-42, 22), (41, 11), (42, 20)):
at = x, 96, z
if not before.state(*at).startswith('minecraft:stone_brick_wall['):
raise ValueError(f'Joint pier baseline changed at {at}')
details[at], groups[at] = 'minecraft:cut_sandstone', 'finished-road-joint-piers'
# Allow the complete plaza, its balustrade and short level approach aprons.
near = {(x+dx, z+dz) for x, z in outer for dx in range(-2, 3) for dz in range(-2, 3)}
interior = outer | {tuple(p) for p in rails['fence_columns']}
interior |= {(c['x'], c['z']) for c in geo['cells'] if c['kind'] == 'full' and c['block_y'] == 95
and (c['x'], c['z']) in near}
# Fold the six-face membrane inward around partial collision shapes instead
# of replacing visible rails, stair treads, plants or fixture overhangs.
n6 = ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1))
original_volume = {(x, y, z) for x, z in interior for y in range(96, 116)}
volume = set(original_volume)
cache = {}
def observed(p):
if p not in cache:
cache[p] = details.get(p, before.state(*p))
return cache[p]
for _ in range(128):
membrane = {(x+dx, y+dy, z+dz) for x, y, z in volume for dx, dy, dz in n6} - volume
partial = {p for p in membrane if observed(p) != AIR and not full(observed(p))}
if not partial:
break
remove = {(x+dx, y+dy, z+dz) for x, y, z in partial for dx, dy, dz in n6} & volume
if not remove:
raise ValueError('Partial-shape boundary cannot be sealed without editing decoration')
volume -= remove
else:
raise ValueError('Boundary folding did not converge')
if (0, 96, 43) not in volume:
raise ValueError('Spawn is outside the finished enclosure')
shell = {(x, z) for x, y, z in membrane if 96 <= y < 116}
floor = {p for p in membrane if p[1] == 95}
roof = {p for p in membrane if p[1] == 116}
walls = membrane - floor - roof
barriers, barrier_groups = {}, {}
for group, positions in [('invisible-side-wall', walls), ('invisible-floor-gap', floor), ('invisible-roof', roof)]:
for at in sorted(positions):
state = details.get(at, before.state(*at))
if full(state):
continue
if state != AIR:
raise ValueError(f'Enclosure would erase a visible/partial block at {at}: {state}')
barriers[at], barrier_groups[at] = BARRIER, group
if details.keys() & barriers.keys():
raise ValueError('Decoration and containment recipes overlap')
def recipe(states, labels):
return {'version': 1, 'scope': before.scope, 'blocks': [dict(zip(('x', 'y', 'z'), p)) |
{'block': state, 'expected': before.state(*p), 'group': labels[p]}
for p, state in sorted(states.items()) if state != before.state(*p)]}
finish, envelope = recipe(details, groups), recipe(barriers, barrier_groups)
combined = {'version': 1, 'scope': before.scope, 'blocks': finish['blocks']+envelope['blocks']}
metadata = {'version': 1, 'scope': before.scope, 'interior_columns': sorted(interior),
'shell_columns': sorted(shell), 'floor_y': 95, 'roof_y': 116,
'source': {'x': .5, 'y': 96, 'z': 43.5},
'interior_excluded_voxels': sorted(original_volume-volume),
'decoration': {'urn_centers': urns, 'welcome_plinth': [-8, 43], 'thresholds': thresholds},
'barrier_groups': dict(Counter(b['group'] for b in envelope['blocks'])),
'detail_groups': dict(Counter(b['group'] for b in finish['blocks'])),
'scope_note': 'Static collision enclosure for normal players; spectator and operator teleport/break commands bypass it.'}
return finish, envelope, combined, metadata
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--before', type=Path, default=STAGE / 'before.json.gz')
parser.add_argument('--output-dir', type=Path, default=STAGE)
args = parser.parse_args()
result = compile_plans(survey.load_snapshot(args.before))
args.output_dir.mkdir(parents=True, exist_ok=True)
for name, value in zip(('finishing.json', 'barriers.json', 'combined.json', 'zone-boundary.metadata.json'), result):
(args.output_dir/name).write_text(json.dumps(value, separators=(',', ':'))+'\n')
print(json.dumps({'details': len(result[0]['blocks']), 'barriers': len(result[1]['blocks']),
'interior_columns': len(result[3]['interior_columns']), 'shell_columns': len(result[3]['shell_columns']),
'groups': result[3]['barrier_groups']}))
if __name__ == '__main__':
main()
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Read-only elevation study for the first two Shacraft construction districts.
Writes a reviewable design specification and sections. Does not issue world edits.
Run with .runtime/terrain-study/venv/bin/python scripts/foundation-study/design.py.
Y convention: floor_y is the block coordinate; full-block walking height is Y + 1.
"""
import json
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.path import Path as Polygon
from matplotlib.colors import LightSource
ROOT=Path(__file__).resolve().parents[2]
OUT=ROOT/'.runtime/foundation-study/design'
OUT.mkdir(parents=True,exist_ok=True)
layout=json.loads((ROOT/'examples/layout/shacraft-lobby-layout.json').read_text())
actual=json.loads((ROOT/'.runtime/server/plugins/ShacraftTerrain/maps/layout-final.json').read_text())
natural=np.floor(np.fromfile(ROOT/'.runtime/terrain-study/natural.f32',dtype='>f4').reshape(768,768)).astype(int)
live_heights=np.asarray(actual['surface_y']).reshape(768,768)
live_materials=np.asarray(actual['material_index']).reshape(768,768)
zz,xx=np.mgrid[-384:384,-384:384]
points=np.column_stack([xx.ravel(),zz.ravel()])
def feature(fid): return next(f for f in layout['features'] if f['id']==fid)
def route(fid): return next(f for f in layout['routes'] if f['id']==fid)
def polygon_mask(vertices):return Polygon(vertices).contains_points(points,radius=.1).reshape(natural.shape)
def metrics(mask,y):
h=natural[mask]
deltas,counts=np.unique(live_heights[mask]-h,return_counts=True)
actual_delta_counts={str(int(k)):int(v) for k,v in zip(deltas,counts)}
return {'columns':int(mask.sum()),'live_surface_delta_from_natural_counts':actual_delta_counts,'natural_min_y':int(h.min()),'natural_max_y':int(h.max()),'cut_above_floor_blocks':int(np.maximum(h-y,0).sum()),'fill_above_natural_blocks':int(np.maximum(y-h,0).sum()),'maximum_cut':int(max(0,(h-y).max())),'maximum_fill':int(max(0,(y-h).max()))}
surfaces=[]
for fid,y in [('arrival-hex',95),('station-forecourt',95),('clock-station',98)]:
f=feature(fid); mask=polygon_mask(f['points'])
surfaces.append({'id':fid,'district':f['district'],'geometry':'polygon','points':f['points'],'floor_y':y,'walk_y':y+1,'support':'Solid to existing ground inside the exact footprint; two-deep structural deck in cut areas. Exterior dark stone masonry, light stone coping, no broad earth platform.','metrics':metrics(mask,y)})
# Each schedule is expressed in the increasing travel coordinate d, irrespective of
# whether the selected world axis increases or decreases along travel. A stair
# replaces the current high-level full block; its back points toward the higher
# previous row. The next row is one full block lower. This guarantees 0.5 steps.
def descent(fid,axis,sign,start,end,y,stairs,final_y,width,waypoints):
rows=[]; level=y
for a in range(start*sign,end*sign+1):
c=a*sign
is_stair=c in stairs
facing=('north' if axis=='z' else ('west' if sign==1 else 'east'))
row={'coordinate':c,'block_y':level,'kind':'stairs' if is_stair else 'full','walk_high_y':level+1,'walk_low_y':level+.5 if is_stair else level+1}
if is_stair:row['facing']=facing
rows.append(row)
if is_stair:level-=1
assert level==final_y,(fid,level,final_y)
# Walking from higher to lower: flat->stair upper edge is same elevation;
# stair upper->lower edge is 0.5, lower->next flat is another 0.5.
for a,b in zip(rows,rows[1:]):assert abs(a['walk_low_y']-b['walk_high_y'])<=.5
return {'id':fid,'geometry':'road_profile','axis':axis,'travel_sign':sign,'width':width,'width_note':'Clear paving width, with one additional coping block outside either edge; use the existing marked centerline tangent.','waypoints':waypoints,'start_floor_y':y,'end_floor_y':final_y,'rows':rows,'max_walk_step':.5,'scope':'Only this local part is built in stage 01; preserve all later district markers.'}
roads=[
{'id':'station-axis','geometry':'road_flat','width':9,'floor_y':95,'walk_y':96,'waypoints':route('station-axis')['waypoints'],'profile_until_z':-77,'intent':'Continuous flat link into the forecourt; ornamental edge strips outside nine clear blocks.'},
descent('south-axis-local','z',1,57,110,95,[58,61,64,67,70,73,76,79,82,86,90,94,98,101,104,107],79,9,[[0,57],[3,97],[1,110]]),
descent('portal-radial-local','x',-1,-35,-72,95,[-42,-47,-52,-58,-64,-71],89,7,[[-35,-14],[-67,-43],[-72,-46]]),
descent('lake-radial-local','x',-1,-42,-90,95,[-44,-47,-50,-55,-64,-72,-78,-83,-85,-87,-89,-90],83,5,[[-42,18],[-77,27],[-90,32]]),
descent('east-radial-local','x',1,42,82,95,[43,47,51,55,60,66,72,78],87,7,[[42,16],[65,10],[82,8]]),
descent('ring-northwest-local','x',-1,-40,-80,95,[-42,-45,-49,-53,-57,-61,-65,-69,-73,-77],85,7,[[-40,-65],[-77,-76],[-80,-77]]),
descent('ring-northeast-local','x',1,40,82,98,[41,44,47,50,54,58,62,66,70,74,78,81],86,7,[[40,-95],[60,-91],[82,-78]])
]
# Endpoint toes use actual captured surface elevations. At the lake the last
# in-bounds column is a stair: a full block here would leave a full-block drop to
# the next native row, and editing that row would exceed the stage's X=-90 bound.
endpoint_toes=[]
for rid,end in [('portal-radial-local',(-72,-46)),('lake-radial-local',(-90,32))]:
r=next(item for item in roads if item['id']==rid)
last=r['rows'][-1]
native=[]
for z in range(end[1]-1,end[1]+2):
x=end[0]-1
native.append({'x':x,'z':z,'natural_block_y':int(natural[z+384,x+384]),'live_surface_y':int(live_heights[z+384,x+384]),'step_from_road':abs(last['walk_low_y']-(int(live_heights[z+384,x+384])+1))})
assert all(n['step_from_road']<=.5 for n in native),(rid,native)
r['endpoint_kind']=last['kind']
r['endpoint_block_y']=last['block_y']
r['endpoint_walk_low_y']=last['walk_low_y']
r['native_toe']=native
endpoint_toes.append({'id':rid,'last_road_column':list(end),'last_road_kind':last['kind'],'last_road_block_y':last['block_y'],'native_front_three':native})
stairs=[
{'z':-77,'block_y':95,'kind':'full'},
{'z':-78,'block_y':96,'kind':'stairs','facing':'north'},
{'z':-79,'block_y':96,'kind':'full'},
{'z':-80,'block_y':97,'kind':'stairs','facing':'north'},
{'z':-81,'block_y':97,'kind':'full'},
{'z':-82,'block_y':98,'kind':'stairs','facing':'north'},
{'z':-83,'block_y':98,'kind':'full'}]
plan={
'schema':'shacraft-foundation-elevation-design-v1',
'status':'DESIGN ONLY; root builder must compare actual voxels before applying and verify final live world.',
'world':actual['world'],'source_capture_finished_at':actual['capture_finished_at'],
'y_convention':'floor_y/block_y is the Minecraft block coordinate. A full block at Y95 is walked on at Y96. A bottom stair at Y96 spans feet heights96.5..97.',
'selected_districts':['01','02'],
'surfaces':surfaces,'roads':roads,'verified_native_endpoint_toes':endpoint_toes,
'station_entrance_stair':{'x_min':-16,'x_max':4,'width':21,'travel_direction':'north','rows':stairs,'max_walk_step':.5},
'station_ne_connection':{'geometry':'polygon','points':[[35,-103],[42,-103],[44,-95],[42,-91],[35,-94],[35,-103]],'floor_y':98,'intent':'Small local upper landing joins east wing to the northeast descending road. Keep inside this polygon; no platform around the whole station.'},
'plaza_inlay':{'center':[0,9],'outer_radius':16,'floor_y':95,'intent':'Flat green hexagonal ring with cream Shacraft S inlay. Reserve the core for later fountain/arrival feature if desired; never raise the inlay above paving.'},
'masonry':{'foundation_core':'minecraft:stone','dark_footing':'minecraft:deepslate_bricks','wall':'minecraft:stone_bricks','secondary_wall':'minecraft:andesite','light_coping':'minecraft:smooth_sandstone','paving':'minecraft:smooth_sandstone','paving_bands':'minecraft:smooth_stone','accent':'minecraft:green_concrete','notes':['Do not fill the surrounding rectangle. Shape all exposed plinth walls to the exact existing footprint.','The west station wing has up to16 blocks of foundation. Use vertical pilasters every8 blocks and recessed blind arch panels; one plain flat wall would look excessive.','Leave district02 building floor usable and flat. Footing lines can indicate tower and pavilions; no tall unfinished walls across doors.','Put parapets only on exposed drops, outside the clear walking width; leave all route openings unblocked.','Finish side road ends with a full-width landing; east87/northeast86 match the existing future bridge levels, so keep the boundary to those markers precise.']},
'quality_checks':['Actual block-for-block scan of finished footprint and road surfaces.','Cardinal-direction walking graph from spawn to station floor and every road endpoint, including stair orientation and at least2 air blocks headroom.','Every descent has half-block treads; never create a full block jump as a path transition.','Foundation columns extend down to solid existing ground; no hidden unsupported floating perimeter.','No new block in a water column; no edit outside foundation/road footprints except explicitly approved local rail, light and footing cells.','Check road width across diagonal curves; evaluate the union of row footprints rather than nearest centerline points alone.','Remove obsolete colored markers and text only inside the stage01 mutation envelope; preserve all other district reservations.','Compare orthographic live surface map with plan and inspect actual west station foundation + north axis from player height.'],
'endpoint_join_notes':['South road ends at z110/block79 near actual ground78..80; a few local grading cells or one final stair row may be required after live voxel inspection.','Portal endpoint(-72,-46) fullY89 joins native89. Lake endpoint(-90,32) uses terminal east-facing stairY84, joining native83 beyond x-90 across the center3 cells with half-block steps; never replace this terminal stair with a full-block landing.','Northeast road must originate at the station upper landing98, not the forecourt95.','The existing camera is spectator; physical walking correctness still requires geometric collision checks.']
}
(OUT/'foundation-design.json').write_text(json.dumps(plan,indent=2)+'\n')
# Inspectable plan/sections are computed from immutable natural heights.
# Live surface deltas are measured separately in the JSON metrics above.
fig=plt.figure(figsize=(15,11),layout='constrained')
gs=fig.add_gridspec(2,2,width_ratios=[1.22,1],height_ratios=[1,1])
ax=fig.add_subplot(gs[:,0]); extent=(-110,105,125,-165)
h=natural[219:510,274:490]
shade=LightSource(315,40).hillshade(h,vert_exag=2,dx=1,dy=1)
ax.imshow(h,cmap='gist_earth',extent=extent,alpha=.8,vmin=48,vmax=150)
ax.imshow(shade,cmap='gray',extent=extent,alpha=.2)
colors=['#f2dfbd','#ddd2b6','#f0c75b']
for s,c in zip(surfaces,colors):
pp=np.array(s['points']); ax.fill(pp[:,0],pp[:,1],c,alpha=.8);ax.plot(pp[:,0],pp[:,1],color='#263e39',lw=1.2)
cc=pp.mean(axis=0);ax.text(cc[0],cc[1],s['id'].replace('-',' ')+'\nblock Y'+str(s['floor_y']),ha='center',va='center',fontsize=9,bbox=dict(facecolor='white',alpha=.8,edgecolor='none'))
for r in roads:
p=np.array(r['waypoints']);ax.plot(p[:,0],p[:,1],color='#ede3cf',lw=r['width']*.55,solid_capstyle='round');ax.plot(p[:,0],p[:,1],color='#485b53',lw=.6)
if 'end_floor_y' in r:ax.text(p[-1,0],p[-1,1],'Y'+str(r.get('endpoint_block_y',r['end_floor_y']))+(' stair' if r.get('endpoint_kind')=='stairs' else ''),fontsize=8,ha='center',va='bottom',bbox=dict(facecolor='white',alpha=.85,edgecolor='none'))
ax.set(xlim=(-105,100),ylim=(125,-165),xlabel='X — east →',ylabel='Z — south →',title='Shacraft · stage 01 foundations and local streets')
ax.set_aspect('equal');ax.grid(alpha=.15)
ax2=fig.add_subplot(gs[0,1]);xs=np.arange(-80,47); zs=-120*np.ones_like(xs); ground=natural[zs+384,xs+384]
ax2.fill_between(xs,ground,70,color='#718356',alpha=.65,label='Existing natural ground'); ax2.plot(xs,ground,color='#354d2c',lw=1)
inside=(xs>=-74)&(xs<=40);ax2.plot(xs[inside],np.full(sum(inside),99),color='#ae7731',lw=2,label='Station walking plane Y99');ax2.fill_between(xs[inside],ground[inside],99,color='#c4bda9',alpha=.6,label='Supported station plinth')
ax2.set(xlabel='X across station at Z−120',ylabel='Y elevation',ylim=(78,103),title='Station: one continuous floor; articulated west retaining base');ax2.legend(fontsize=8,loc='lower right');ax2.grid(alpha=.2)
ax3=fig.add_subplot(gs[1,1]);zvals=np.arange(-100,111);xvals=np.where(zvals<0,-6,0);ground=natural[zvals+384,xvals+384];ax3.fill_between(zvals,ground,65,color='#718356',alpha=.65)
walk=np.full(len(zvals),96.);walk[zvals<=-83]=99
for row in stairs:
k=np.where(zvals==row['z'])[0][0];walk[k]=row['block_y']+(0.75 if row['kind']=='stairs' else 1)
for row in roads[1]['rows']:
k=np.where(zvals==row['coordinate'])[0][0];walk[k]=(row['walk_low_y']+row['walk_high_y'])/2
ax3.plot(zvals,walk,color='#a47230',lw=2,label='Planned walking surface');ax3.plot(zvals,ground,color='#354d2c',lw=1,label='Natural centerline')
ax3.set(xlabel='Z along north/south arrival route',ylabel='Y elevation',ylim=(74,103),title='Continuous arrival sequence: station → forecourt → square → approach');ax3.grid(alpha=.2);ax3.legend(fontsize=8)
fig.savefig(OUT/'foundation-plan-and-sections.png',dpi=170)
print(json.dumps({'plan':str(OUT/'foundation-design.json'),'figure':str(OUT/'foundation-plan-and-sections.png'),'surfaces':[{k:v for k,v in s.items() if k in ['id','floor_y','metrics']} for s in surfaces]},indent=2))
+276
View File
@@ -0,0 +1,276 @@
#!/usr/bin/env python3
"""Rasterize the reviewed Shacraft stage-one design without editing the world.
The public build_geometry(design, layout) function uses only the Python standard
library. Coordinate keys are (x, z). Each cell has block_y, kind, group and clear;
straight bottom stairs additionally have facing. block_y is not player feet Y.
Precedence: exact foundation polygons, northeast landing, local roads, central
station staircase. Existing dense approved centerlines are used for road curves.
Roads extend a few flat rows back into their origin plaza so an avenue cannot
pinch down to the one-block vertex of the hexagonal square.
"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
from typing import Any
Point = tuple[int, int]
Cell = dict[str, Any]
def _on_segment(x: int, z: int, a, b) -> bool:
cross = (x-a[0])*(b[1]-a[1]) - (z-a[1])*(b[0]-a[0])
return abs(cross) < 1e-8 and min(a[0],b[0]) <= x <= max(a[0],b[0]) and min(a[1],b[1]) <= z <= max(a[1],b[1])
def polygon_cells(vertices) -> set[Point]:
"""Integer block columns inside OR on the exact polygon, with no box fill."""
vertices = [tuple(p) for p in vertices]
if len(vertices) < 3:
raise ValueError('A foundation polygon needs three vertices')
segments = list(zip(vertices, vertices[1:]+vertices[:1]))
result: set[Point] = set()
for z in range(math.floor(min(p[1] for p in vertices)), math.ceil(max(p[1] for p in vertices))+1):
for x in range(math.floor(min(p[0] for p in vertices)), math.ceil(max(p[0] for p in vertices))+1):
inside = False
boundary = False
for a,b in segments:
if _on_segment(x,z,a,b):
boundary = True
break
if (a[1] > z) != (b[1] > z):
cross_x = a[0] + (z-a[1])*(b[0]-a[0])/(b[1]-a[1])
if x < cross_x:
inside = not inside
if boundary or inside:
result.add((x,z))
return result
def _distance_squared(x: int, z: int, a, b) -> float:
dx,dz=b[0]-a[0],b[1]-a[1]
if not dx and not dz:
return (x-a[0])**2+(z-a[1])**2
t=max(0.,min(1.,((x-a[0])*dx+(z-a[1])*dz)/(dx*dx+dz*dz)))
return (x-a[0]-t*dx)**2+(z-a[1]-t*dz)**2
def _densify(points) -> list[Point]:
result=[]
for a,b in zip(points,points[1:]):
n=max(1,math.ceil(max(abs(a[0]-b[0]),abs(a[1]-b[1]))))
for i in range(n+1):
p=(round(a[0]+(b[0]-a[0])*i/n),round(a[1]+(b[1]-a[1])*i/n))
if not result or result[-1] != p:
result.append(p)
if not result and points:
result=[tuple(map(round,points[0]))]
return result
def _cardinal_path(points: list[Point]) -> list[Point]:
"""Insert cardinal intermediate samples for collision checks on diagonals."""
if not points:
return []
result=[points[0]]
for x,z in points[1:]:
ax,az=result[-1]
while (ax,az)!=(x,z):
if ax!=x:
ax += 1 if x>ax else -1
else:
az += 1 if z>az else -1
result.append((ax,az))
return result
def _approved_centerline(spec, layout) -> list[Point]:
base_id=spec['id'].removesuffix('-local')
approved=next((r for r in layout.get('routes',[]) if r['id']==base_id),None)
points=_densify(approved['points'] if approved else spec['waypoints'])
if spec['geometry']=='road_profile':
axis=0 if spec['axis']=='x' else 1
lo=min(row['coordinate'] for row in spec['rows'])
hi=max(row['coordinate'] for row in spec['rows'])
points=[p for p in points if lo <= p[axis] <= hi]
# If the approved path terminates just before a reviewed local endpoint,
# add the small explicit tail without replacing its established curve.
expected=tuple(spec['waypoints'][-1])
if points and points[-1][axis] != expected[axis]:
points += _densify([points[-1],expected])[1:]
elif 'profile_until_z' in spec:
stop=spec['profile_until_z']
points=[p for p in points if p[1]>=stop]
if len(points)<2:
raise ValueError(f"No usable centerline for {spec['id']}")
return points
def _origin_extension(points: list[Point], spec) -> list[Point]:
"""Overlap the origin plateau without changing its level or approved curve."""
first=points[0]
distance=max(4,spec['width']//2+2)
target=points[min(len(points)-1,distance)]
dx,dz=target[0]-first[0],target[1]-first[1]
length=math.hypot(dx,dz)
if not length:
return points
back=(round(first[0]-dx*distance/length),round(first[1]-dz*distance/length))
return _densify([back,first])[:-1]+points
def _corridor(points: list[Point], width: int, axis=None, lo=None, hi=None):
"""Clear corridor plus one block of side coping, evaluated by cell centers."""
outer=width/2+1
clear_radius2=(width/2)**2
outer_radius2=outer**2
distances: dict[Point,float] = {}
for a,b in zip(points,points[1:]):
for z in range(math.floor(min(a[1],b[1])-outer),math.ceil(max(a[1],b[1])+outer)+1):
for x in range(math.floor(min(a[0],b[0])-outer),math.ceil(max(a[0],b[0])+outer)+1):
if axis is not None and not lo <= (x,z)[axis] <= hi:
continue
d=_distance_squared(x,z,a,b)
if d <= outer_radius2+1e-8 and d < distances.get((x,z),math.inf):
distances[(x,z)]=d
return {p:d<=clear_radius2+1e-8 for p,d in distances.items()}
def tread_height(cell: Cell, local_x: float, local_z: float) -> float:
"""Collision surface of a full block or a straight bottom stair at an offset.
Use .25/.75 offsets to inspect both tread halves, avoiding the central edge.
"""
if cell['kind']=='full':
return cell['block_y']+1
high={'north':local_z<.5,'south':local_z>.5,
'west':local_x<.5,'east':local_x>.5}[cell['facing']]
return cell['block_y']+(1. if high else .5)
def build_geometry(design, layout):
"""Return cells, route metadata and compact invariant checks.
cells[(x,z)] -> {block_y:int,kind:'full'|'stairs',facing?:str,
group:str,clear:bool}
routes[] -> {id,centerline,centerline_4,corridor,clear_cells,clear_width}
`clear` distinguishes usable road paving from exterior coping; for a main
polygon every floor cell is clear. It does not mean the world has been cleared.
"""
cells: dict[Point,Cell] = {}
routes=[]
polygon_areas={}
for surface in design['surfaces']:
footprint=polygon_cells(surface['points'])
polygon_areas[surface['id']]=len(footprint)
for p in footprint:
cells[p]={'block_y':surface['floor_y'],'kind':'full','group':surface['id'],'clear':True}
landing=design.get('station_ne_connection')
if landing:
for p in polygon_cells(landing['points']):
cells[p]={'block_y':landing['floor_y'],'kind':'full','group':'station-ne-connection','clear':True}
for spec in design['roads']:
centerline=_approved_centerline(spec,layout)
extended=_origin_extension(centerline,spec)
profiled=spec['geometry']=='road_profile'
if profiled:
axis=0 if spec['axis']=='x' else 1
profiles={row['coordinate']:row for row in spec['rows']}
# Extend only the origin; stop exactly at the designed last road row.
endpoint=centerline[-1][axis]
origin=extended[0][axis]
lo,hi=sorted([endpoint,origin])
corridor=_corridor(extended,spec['width'],axis,lo,hi)
first=spec['rows'][0]
else:
corridor=_corridor(extended,spec['width'])
# The last flat avenue rows must not cover the station staircase.
if 'profile_until_z' in spec:
corridor={p:clear for p,clear in corridor.items() if p[1]>=spec['profile_until_z']}
for p,clear in corridor.items():
if profiled:
row=profiles.get(p[axis])
if row is None:
row={'block_y':first['block_y'],'kind':'full'}
cell={'block_y':row['block_y'],'kind':row['kind'],'group':spec['id'],'clear':clear}
if row['kind']=='stairs':
cell['facing']=row['facing']
else:
cell={'block_y':spec['floor_y'],'kind':'full','group':spec['id'],'clear':clear}
# A coping line within an already level clear plaza is a floor band,
# not an obstacle or a place for a parapet. Preserve that distinction.
previous=cells.get(p)
if previous and previous['clear'] and previous['block_y']==cell['block_y'] and previous['kind']==cell['kind']=='full':
cell['clear']=True
cells[p]=cell
routes.append({'id':spec['id'],'centerline':centerline,'centerline_4':_cardinal_path(centerline),'corridor':sorted(corridor),'clear_cells':sorted(p for p,c in corridor.items() if c),'clear_width':spec['width']})
stair=design['station_entrance_stair']
stair_cells=[]
stair_clear=[]
for row in stair['rows']:
for x in range(stair['x_min']-1,stair['x_max']+2):
p=(x,row['z'])
clear=stair['x_min']<=x<=stair['x_max']
cells[p]={'block_y':row['block_y'],'kind':row['kind'],'group':'station-entrance-stair','clear':clear}
if row['kind']=='stairs':
cells[p]['facing']=row['facing']
stair_cells.append(p)
if clear:
stair_clear.append(p)
mid=(stair['x_min']+stair['x_max'])//2
line=[(mid,row['z']) for row in stair['rows']]
routes.append({'id':'station-entrance-stair','centerline':line,'centerline_4':_cardinal_path(line),'corridor':stair_cells,'clear_cells':stair_clear,'clear_width':stair['width']})
missing=[]
blocked=[]
for route in routes:
for p in route['centerline_4']:
if p not in cells:
missing.append((route['id'],p))
elif not cells[p]['clear']:
blocked.append((route['id'],p))
if missing or blocked:
raise ValueError(f'Road centerline incomplete: missing={missing[:8]}, non-clear={blocked[:8]}')
max_step=0.
for route in routes:
path=route['centerline_4']
route_step=0.
for p,q in zip(path,path[1:]):
dx,dz=q[0]-p[0],q[1]-p[1]
departure=tread_height(cells[p],.5+dx*.25,.5+dz*.25)
arrival=tread_height(cells[q],.5-dx*.25,.5-dz*.25)
step=abs(departure-arrival)
if step>.5:
raise ValueError(f"Unwalkable centerline in {route['id']}: {p}->{q}, {step} blocks")
route_step=max(route_step,step)
route['maximum_centerline_step']=route_step
max_step=max(max_step,route_step)
counts={}
for cell in cells.values():
counts[cell['group']]=counts.get(cell['group'],0)+1
return {'cells':cells,'routes':routes,'checks':{'columns':len(cells),'polygon_areas':polygon_areas,'columns_by_final_group':counts,'centerline_missing':len(missing),'centerline_nonclear':len(blocked),'maximum_centerline_step':max_step,'stairs':sum(c['kind']=='stairs' for c in cells.values()),'minimum_block_y':min(c['block_y'] for c in cells.values()),'maximum_block_y':max(c['block_y'] for c in cells.values()),'world_edits':0}}
def serializable(geometry):
return {**geometry,'cells':[{'x':x,'z':z,**cell} for (x,z),cell in sorted(geometry['cells'].items(),key=lambda p:(p[0][1],p[0][0]))]}
def main():
root=Path(__file__).resolve().parents[2]
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--design',type=Path,default=root/'.runtime/foundation-study/design/foundation-design.json')
parser.add_argument('--layout',type=Path,default=root/'examples/layout/shacraft-lobby-layout.json')
parser.add_argument('--output',type=Path,default=root/'.runtime/foundation-study/design/geometry.json')
args=parser.parse_args()
geometry=build_geometry(json.loads(args.design.read_text()),json.loads(args.layout.read_text()))
args.output.parent.mkdir(parents=True,exist_ok=True)
args.output.write_text(json.dumps(serializable(geometry),separators=(',',':'))+'\n')
print(json.dumps({'output':str(args.output),'checks':geometry['checks']},indent=2))
if __name__=='__main__':
main()
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""Independently audit planned foundation navigation on a half-block surface grid.
This is geometry QA, not a Minecraft collision simulator or a live block survey.
Only clear cells are traversable. Four quarter-center samples describe each full
block or straight bottom stair; adjacent samples require <= 0.5-block height change.
"""
import argparse
from collections import Counter, deque
import hashlib
import json
from pathlib import Path
import unittest
def normalize_cells(document):
result = {}
for cell in document['cells']:
if any(type(cell.get(key)) is not int for key in ('x', 'z', 'block_y')):
raise ValueError('Geometry coordinates and block_y must be integers')
if type(cell.get('clear')) is not bool or cell.get('kind') not in ('full', 'stairs'):
raise ValueError('Geometry needs explicit clear and full/stairs kind')
if cell['kind'] == 'stairs' and cell.get('facing') not in ('north', 'east', 'south', 'west'):
raise ValueError('Stairs need an explicit cardinal facing')
at = (cell['x'], cell['z'])
if at in result:
raise ValueError(f'Duplicate geometry cell {at}')
result[at] = cell
return result
def cell_nodes(x, z):
return [(2 * x + i, 2 * z + j) for i in (0, 1) for j in (0, 1)]
def height2(cell, sample):
if cell['kind'] == 'full':
return 2 * cell['block_y'] + 2
ix, iz = sample[0] % 2, sample[1] % 2
facing = cell['facing']
high = ((facing == 'north' and iz == 0) or (facing == 'south' and iz == 1)
or (facing == 'west' and ix == 0) or (facing == 'east' and ix == 1))
return 2 * cell['block_y'] + (2 if high else 1)
def surface_graph(cells):
return {node: height2(cell, node) for (x, z), cell in cells.items() if cell['clear']
for node in cell_nodes(x, z)}
def neighbors(node):
x, z = node
return ((x - 1, z), (x + 1, z), (x, z - 1), (x, z + 1))
def reachable(graph, source):
starts = [p for p in cell_nodes(*source) if p in graph]
if len(starts) != 4:
raise ValueError(f'Navigation source {source} is missing or non-clear')
# One source prevents accidentally joining two disconnected halves of a cell.
queue, reached = deque(starts[:1]), {starts[0]: 0}
while queue:
node = queue.popleft()
for other in neighbors(node):
if other in graph and other not in reached and abs(graph[node] - graph[other]) <= 1:
reached[other] = reached[node] + 1
queue.append(other)
return reached
def at_node(node, height=None):
value = {'x': node[0] / 2 + .25, 'z': node[1] / 2 + .25}
if height is not None:
value['standing_y'] = height / 2
return value
def endpoint_report(name, at, graph, reached):
nodes = cell_nodes(*at)
existing = [p for p in nodes if p in graph]
connected = [p for p in nodes if p in reached]
return {'name': name, 'x': at[0], 'z': at[1], 'surface_samples': len(existing),
'reachable_samples': len(connected), 'passed': len(connected) == 4,
'shortest_surface_route_blocks': min((reached[p] / 2 for p in connected), default=None)}
def route_report(route, cells, graph, reached):
path = [tuple(p) for p in route.get('centerline_4', route['centerline'])]
corridor = {tuple(p) for p in route['corridor']}
clear = {tuple(p) for p in route['clear_cells']}
width = route['clear_width']
if type(width) is not int or width < 1 or width % 2 != 1:
raise ValueError('This cross-section audit requires positive odd clear_width')
missing_corridor = sorted(corridor - cells.keys())
nonclear = sorted(p for p in clear if p not in cells or not cells[p]['clear'])
unreachable = sorted(p for p in clear if any(n not in reached for n in cell_nodes(*p)))
jumps, invalid_steps = [], []
max_boundary_step = 0
for p, q in zip(path, path[1:]):
dx, dz = q[0] - p[0], q[1] - p[1]
if abs(dx) + abs(dz) != 1:
invalid_steps.append({'from': list(p), 'to': list(q)})
continue
# Check both parallel quarter-center lanes across the shared block face.
for a in cell_nodes(*p):
b = (a[0] + dx, a[1] + dz)
if (b[0] // 2, b[1] // 2) != q:
continue
if a not in graph or b not in graph:
jumps.append({'from': at_node(a), 'to': at_node(b), 'reason': 'missing_clear_surface'})
continue
step = abs(graph[a] - graph[b]) / 2
max_boundary_step = max(max_boundary_step, step)
if step > .5:
jumps.append({'from': at_node(a, graph[a]), 'to': at_node(b, graph[b]),
'height_change': step, 'reason': 'height_step_exceeds_half_block'})
cross_sections, narrow = [], []
for i, p in enumerate(path):
# Use at least one corridor-width of tangent support. Inserted cardinal
# elbows near an axis-clipped endpoint must not rotate the cross-section
# to measure longitudinally past that deliberate terminal boundary.
window = max(6, width)
before, after = path[max(0, i - window)], path[min(len(path) - 1, i + window)]
tx, tz = after[0] - before[0], after[1] - before[1]
normal = (0, 1) if abs(tx) > abs(tz) else (1, 0)
radius = width // 2
samples = [(p[0] + normal[0] * offset, p[1] + normal[1] * offset)
for offset in range(-radius, radius + 1)]
present = sum(s in cells for s in samples)
usable = sum(s in cells and cells[s]['clear'] for s in samples)
connected = sum(all(n in reached for n in cell_nodes(*s)) for s in samples)
cross_sections.append((present, usable, connected))
if min(present, usable, connected) < width:
narrow.append({'x': p[0], 'z': p[1], 'normal': list(normal), 'required_width': width,
'structural_columns': present, 'clear_columns': usable,
'reachable_columns': connected,
'problem_columns': [list(s) for s in samples if s not in cells or not cells[s]['clear']
or any(n not in reached for n in cell_nodes(*s))]})
ends = [endpoint_report(route['id'] + ':' + name, at, graph, reached)
for name, at in [('start', path[0]), ('end', path[-1])]]
passed = not (missing_corridor or nonclear or unreachable or jumps or invalid_steps or narrow)
return {'id': route['id'], 'passed': passed, 'declared_clear_width': width,
'corridor_columns': len(corridor), 'missing_corridor_columns': missing_corridor,
'declared_clear_columns': len(clear), 'nonclear_declared_columns': nonclear,
'unreachable_declared_columns': unreachable,
'cross_sections': len(cross_sections),
'minimum_structural_cross_section': min(row[0] for row in cross_sections),
'minimum_clear_cross_section': min(row[1] for row in cross_sections),
'minimum_reachable_cross_section': min(row[2] for row in cross_sections),
'narrow_cross_sections': narrow, 'centerline_invalid_steps': invalid_steps,
'centerline_maximum_boundary_step': max_boundary_step,
'centerline_jumps': jumps, 'endpoints': ends}
def audit(document, source=(0, 9), station=(-6, -105)):
cells = normalize_cells(document)
graph = surface_graph(cells)
reached = reachable(graph, source)
unreachable = sorted(p for p, cell in cells.items() if cell['clear']
and any(n not in reached for n in cell_nodes(*p)))
routes = [route_report(route, cells, graph, reached) for route in document['routes']]
goal = endpoint_report('clock-station', station, graph, reached)
return {'version': 1, 'source': {'x': source[0], 'z': source[1]}, 'world_edits': 0,
'method': 'Four quarter-center surface samples per clear cell; cardinal half-block BFS, rise/drop <= 0.5 block.',
'limitations': 'Planned surface topology only. No headroom, body-width collision, material state or live-world verification. Width checks are cardinal cross-sections using the dominant local tangent; full declared masks are also checked.',
'geometry_sha256': hashlib.sha256(json.dumps(document, sort_keys=True, separators=(',', ':')).encode()).hexdigest(),
'planned_columns': len(cells), 'clear_columns': sum(c['clear'] for c in cells.values()),
'surface_samples': len(graph), 'reachable_samples': len(reached),
'unreachable_clear_columns': [list(p) for p in unreachable],
'unreachable_by_group': dict(Counter(cells[p].get('group', 'unknown') for p in unreachable)),
'station': goal, 'routes': routes,
'passed': not unreachable and goal['passed'] and all(r['passed'] for r in routes)}
class GeometryAuditTests(unittest.TestCase):
def test_full_block_jump_separates_components(self):
cells = {(x, 0): {'kind': 'full', 'block_y': 10 if x == 0 else 11, 'clear': True}
for x in range(2)}
graph = surface_graph(cells)
self.assertEqual(len(reachable(graph, (0, 0))), 4)
def test_stairs_connect_two_levels_for_all_directions(self):
for facing, direction in [('north', (0, -1)), ('south', (0, 1)), ('west', (-1, 0)), ('east', (1, 0))]:
with self.subTest(facing=facing):
dx, dz = direction
cells = {(-dx, -dz): {'kind': 'full', 'block_y': 9, 'clear': True},
(0, 0): {'kind': 'stairs', 'block_y': 10, 'facing': facing, 'clear': True},
(dx, dz): {'kind': 'full', 'block_y': 10, 'clear': True}}
graph = surface_graph(cells)
self.assertEqual(len(reachable(graph, (-dx, -dz))), 12)
def test_nonclear_surface_is_never_traversable(self):
graph = surface_graph({(0, 0): {'kind': 'full', 'block_y': 10, 'clear': False}})
self.assertFalse(graph)
def test_missing_width_column_is_reported(self):
cells = {(x, z): {'x': x, 'z': z, 'kind': 'full', 'block_y': 10, 'clear': True}
for x in range(-1, 2) for z in range(3)}
del cells[(-1, 1)]
graph = surface_graph(cells)
route = {'id': 'test', 'centerline': [(0, 0), (0, 1), (0, 2)],
'corridor': list(cells), 'clear_cells': list(cells), 'clear_width': 3}
report = route_report(route, cells, graph, reachable(graph, (0, 0)))
self.assertFalse(report['passed'])
self.assertEqual(report['minimum_structural_cross_section'], 2)
self.assertEqual(report['narrow_cross_sections'][0]['problem_columns'], [[-1, 1]])
def main():
root = Path(__file__).resolve().parents[2]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--geometry', type=Path, default=root / '.runtime/foundation-study/design/geometry.json')
parser.add_argument('--report', type=Path, default=root / '.runtime/foundation-study/design/navigation-qa.json')
parser.add_argument('--source', type=int, nargs=2, default=(0, 9), metavar=('X', 'Z'))
parser.add_argument('--station', type=int, nargs=2, default=(-6, -105), metavar=('X', 'Z'))
parser.add_argument('--self-test', action='store_true')
args = parser.parse_args()
if args.self_test:
result = unittest.TextTestRunner().run(unittest.defaultTestLoader.loadTestsFromTestCase(GeometryAuditTests))
raise SystemExit(0 if result.wasSuccessful() else 1)
report = audit(json.loads(args.geometry.read_text()), tuple(args.source), tuple(args.station))
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps({'passed': report['passed'], 'clear_columns': report['clear_columns'],
'reachable_samples': report['reachable_samples'], 'surface_samples': report['surface_samples'],
'unreachable_clear_columns': len(report['unreachable_clear_columns']),
'unreachable_by_group': report['unreachable_by_group'], 'station': report['station'],
'routes': [{'id': r['id'], 'passed': r['passed'],
'minimum_clear_width': r['minimum_clear_cross_section'],
'narrow_sections': len(r['narrow_cross_sections']),
'centerline_jumps': len(r['centerline_jumps']),
'unreachable_columns': len(r['unreachable_declared_columns'])} for r in report['routes']],
'report': str(args.report.resolve())}, indent=2))
raise SystemExit(0 if report['passed'] else 1)
if __name__ == '__main__':
main()
+405
View File
@@ -0,0 +1,405 @@
#!/usr/bin/env python3
"""Read-only, scoped block surveys for checked foundations and half-block path clearance.
Snapshots contain actual complete block states, never inferred terrain or implicit air.
Reads are sequential and non-atomic: keep edits idle during a survey. Cached reads may
only be reused explicitly with --resume; the original observation times are retained.
"""
import argparse
from collections import defaultdict
from datetime import datetime, timezone
import fcntl
import gzip
import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import re
import tempfile
_spec = importlib.util.spec_from_file_location('foundation_terrain', Path(__file__).with_name('terrain.py'))
terrain = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(terrain)
SCOPE_KEYS = ('project_id', 'world_id', 'world_epoch')
AXES = ('x', 'y', 'z')
STATE = re.compile(r'minecraft:[a-z0-9_]+(?:\[[a-z0-9_=,]+\])?\Z')
MAX_VOXELS = 4_000_000
AIR = {'minecraft:air', 'minecraft:cave_air', 'minecraft:void_air'}
# Conservative shape set. Unknown blocks never count as air or safe support.
FULL = set(('stone cobblestone mossy_cobblestone stone_bricks mossy_stone_bricks '
'cracked_stone_bricks chiseled_stone_bricks smooth_stone granite polished_granite '
'diorite polished_diorite andesite polished_andesite deepslate cobbled_deepslate '
'polished_deepslate deepslate_bricks deepslate_tiles bricks quartz_block quartz_pillar '
'smooth_quartz sandstone cut_sandstone smooth_sandstone red_sandstone terracotta '
'glass tinted_glass obsidian dirt grass_block bedrock oak_planks spruce_planks '
'birch_planks dark_oak_planks oak_log spruce_log birch_log dark_oak_log '
'stripped_oak_log stripped_spruce_log moss_block glowstone gold_block '
'waxed_oxidized_cut_copper barrier').split())
SLABS = {'stone_brick_slab', 'cobblestone_slab', 'oak_slab', 'spruce_slab', 'smooth_stone_slab'}
COLORS = ('white orange magenta light_blue yellow lime pink gray light_gray cyan purple blue brown green red black').split()
FULL.update(color + suffix for color in COLORS for suffix in ('_concrete', '_terracotta', '_stained_glass'))
def utc_now():
return datetime.now(timezone.utc).isoformat()
def point(value):
if not isinstance(value, dict) or any(type(value.get(a)) is not int for a in AXES):
raise ValueError('Coordinates must contain integer x, y, z')
if any(not -(2 ** 31) <= value[a] < 2 ** 31 for a in AXES):
raise ValueError('Coordinates must fit signed 32-bit integers')
return {a: value[a] for a in AXES}
def scope_of(context):
scope = {k: context.get(k) for k in SCOPE_KEYS}
if any(not isinstance(v, str) or not v for v in scope.values()):
raise ValueError('Project context must identify project, world and epoch')
return scope
def volume(box):
return math.prod(box['max'][a] - box['min'][a] + 1 for a in AXES)
def box_of(lo, hi):
box = {'min': point(lo), 'max': point(hi)}
if any(box['min'][a] > box['max'][a] for a in AXES):
raise ValueError('Minimum exceeds maximum')
return box
def cell_key(box):
return tuple(box['min'][a] // 16 for a in AXES)
def box_cells(lo, hi):
box = box_of(lo, hi)
if volume(box) > MAX_VOXELS:
raise ValueError(f'Survey exceeds {MAX_VOXELS:,} voxels; split it explicitly')
cells = []
for cx in range(lo['x'] // 16, hi['x'] // 16 + 1):
for cy in range(lo['y'] // 16, hi['y'] // 16 + 1):
for cz in range(lo['z'] // 16, hi['z'] // 16 + 1):
origin = dict(zip(AXES, (cx * 16, cy * 16, cz * 16)))
cells.append({'min': {a: max(lo[a], origin[a]) for a in AXES},
'max': {a: min(hi[a], origin[a] + 15) for a in AXES}})
return cells
def column_cells(document):
if not isinstance(document, dict) or document.get('version') != 1 or not isinstance(document.get('columns'), list):
raise ValueError('Column plan requires version: 1 and columns: [{x,z,min_y,max_y}]')
if not 1 <= len(document['columns']) <= MAX_VOXELS:
raise ValueError('Column plan is empty or too large')
merged = {}
for column in document['columns']:
if not isinstance(column, dict) or any(type(column.get(k)) is not int for k in ('x', 'z', 'min_y', 'max_y')):
raise ValueError('Every column requires integer x, z, min_y and max_y')
lo = point({'x': column['x'], 'z': column['z'], 'y': column['min_y']})
hi = point({**lo, 'y': column['max_y']})
for box in box_cells(lo, hi):
key = cell_key(box)
old = merged.get(key)
merged[key] = box if old is None else {
'min': {a: min(old['min'][a], box['min'][a]) for a in AXES},
'max': {a: max(old['max'][a], box['max'][a]) for a in AXES}}
cells = [merged[key] for key in sorted(merged)]
if sum(volume(b) for b in cells) > MAX_VOXELS:
raise ValueError('Expanded column survey exceeds voxel limit')
return cells
def assert_in_scope(cells, context):
region = context.get('region', {})
area = box_of(region.get('min'), region.get('max'))
for cell in cells:
if any(cell['min'][a] < area['min'][a] or cell['max'][a] > area['max'][a] for a in AXES):
raise ValueError('Requested survey exceeds the selected project area')
def index_at(box, x, y, z):
lo, hi = box['min'], box['max']
if not (lo['x'] <= x <= hi['x'] and lo['y'] <= y <= hi['y'] and lo['z'] <= z <= hi['z']):
raise KeyError(f'No observed block at {(x, y, z)}; air is never inferred')
return ((y - lo['y']) * (hi['z'] - lo['z'] + 1) + z - lo['z']) * (hi['x'] - lo['x'] + 1) + x - lo['x']
def capture_cell(backend, box, epoch):
started = utc_now()
result = backend.call('region_inspect', **box, detail='blocks')
if result.get('world_epoch') != epoch or result.get('truncated') is not False:
raise RuntimeError('Inspection was truncated or returned another world epoch')
palette, lookup = [], {}
indices = [None] * volume(box)
for item in result.get('blocks', []):
pos = point(item.get('pos'))
index = index_at(box, **pos)
if indices[index] is not None:
raise RuntimeError('Inspection returned a duplicate position')
state = item.get('state')
if not isinstance(state, str) or not STATE.fullmatch(state):
raise RuntimeError('Inspection returned an invalid block state')
if state not in lookup:
lookup[state] = len(palette)
palette.append(state)
indices[index] = lookup[state]
if any(i is None for i in indices):
raise RuntimeError('Inspection omitted blocks; missing blocks are not air')
return {**box, 'palette': palette, 'indices': indices,
'observed_from': started, 'observed_until': utc_now()}
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
def save_gzip(path, document):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
temporary = None
try:
with tempfile.NamedTemporaryFile(prefix=path.name + '.', suffix='.tmp', dir=path.parent, delete=False) as raw:
temporary = Path(raw.name)
with gzip.GzipFile(fileobj=raw, mode='wb', mtime=0) as zipped:
zipped.write(json.dumps(document, separators=(',', ':')).encode())
raw.flush()
os.fsync(raw.fileno())
temporary.replace(path)
directory = os.open(path.parent, os.O_RDONLY)
try:
os.fsync(directory)
finally:
os.close(directory)
finally:
if temporary is not None and temporary.exists():
temporary.unlink()
class Snapshot:
def __init__(self, document, expected_scope=None):
if not isinstance(document, dict) or document.get('version') != 1 or document.get('complete') is not True:
raise ValueError('Only complete version-1 snapshots may be used')
self.scope = scope_of(document.get('scope', {}))
if expected_scope is not None and self.scope != expected_scope:
raise ValueError('Snapshot belongs to another project/world/epoch')
self.document, self.cells = document, {}
requested = document.get('requested_cells')
captured = document.get('cells')
if not isinstance(requested, list) or not requested or not isinstance(captured, list) or len(requested) != len(captured):
raise ValueError('Snapshot does not cover every requested cell')
if sum(volume(box_of(b.get('min'), b.get('max'))) for b in requested) > MAX_VOXELS:
raise ValueError('Snapshot exceeds maximum voxel count')
for expected, cell in zip(requested, captured):
box = box_of(cell.get('min'), cell.get('max'))
if box != expected or cell_key(box) != tuple(box['max'][a] // 16 for a in AXES):
raise ValueError('Snapshot cell differs from request or crosses a 16³ cell')
key = cell_key(box)
if key in self.cells:
raise ValueError('Snapshot contains duplicate cells')
palette, indices = cell.get('palette'), cell.get('indices')
if not isinstance(palette, list) or not palette or any(not isinstance(s, str) or not STATE.fullmatch(s) for s in palette):
raise ValueError('Snapshot palette contains invalid states')
if not isinstance(indices, list) or len(indices) != volume(box) or any(type(i) is not int or not 0 <= i < len(palette) for i in indices):
raise ValueError('Snapshot has missing or invalid block indices')
self.cells[key] = cell
def state(self, x, y, z):
point({'x': x, 'y': y, 'z': z})
cell = self.cells.get((x // 16, y // 16, z // 16))
if cell is None:
raise KeyError(f'No observed block at {(x, y, z)}; air is never inferred')
return cell['palette'][cell['indices'][index_at(cell, x, y, z)]]
def states_for(self, blocks):
return {tuple(b[a] for a in AXES): self.state(**point(b)) for b in blocks}
def load_snapshot(path, expected_scope=None):
with gzip.open(path, 'rt') as handle:
return Snapshot(json.load(handle), expected_scope)
def scan(backend, cells, path, resume=False, progress=lambda value: None):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
context = backend.call('project_context')
scope = scope_of(context)
assert_in_scope(cells, context)
cache = path.with_name(path.name + '.parts')
with path.with_name(path.name + '.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
if path.exists():
raise ValueError('Complete snapshot already exists; choose a fresh path for a fresh survey')
if cache.exists() and not resume:
raise ValueError('Survey cache already exists; --resume explicitly reuses older observations')
cache.mkdir(exist_ok=True)
identity = {'version': 1, 'scope': scope, 'requested_cells': cells}
header = cache / 'manifest.json'
if header.exists():
saved = json.loads(header.read_text())
if saved.get('identity') != identity or saved.get('digest') != digest(identity):
raise ValueError('Survey cache scope or requested cells differ')
else:
saved = {'identity': identity, 'digest': digest(identity), 'started_at': utc_now()}
terrain.save(header, saved)
captured = []
for number, box in enumerate(cells):
part = cache / f'{number:06d}.json.gz'
if part.exists():
with gzip.open(part, 'rt') as handle:
cell = json.load(handle)
# Reject incomplete, reordered, or damaged cached observations before reuse.
Snapshot({'version': 1, 'complete': True, 'scope': scope,
'requested_cells': [box], 'cells': [cell]}, scope)
else:
cell = capture_cell(backend, box, scope['world_epoch'])
save_gzip(part, cell)
captured.append(cell)
progress({'cells': number + 1, 'total_cells': len(cells)})
final_context = backend.call('project_context')
if scope_of(final_context) != scope or final_context.get('region') != context.get('region'):
raise RuntimeError('Project scope changed during capture; no complete snapshot written')
document = {**identity, 'complete': True, 'atomic_snapshot': False,
'started_at': saved['started_at'], 'finished_at': utc_now(),
'resumed_cache': resume, 'cells': captured,
'note': 'Sequential observations. Exact states must be checked again atomically when editing.'}
result = Snapshot(document, scope)
save_gzip(path, document)
return result
def vertical_shape(state, sub_x=.5, sub_z=.5):
"""Occupied Y intervals at a surface sample, or None for unknown geometry.
A straight stair has two levels. Samples on the riser boundary are ambiguous
and rejected, including the default center, rather than choosing one level.
"""
if state in AIR:
return []
base, _, properties = state.removeprefix('minecraft:').partition('[')
props = dict(piece.split('=', 1) for piece in properties.rstrip(']').split(',') if '=' in piece)
if base in FULL:
return [(0.0, 1.0)]
if base in SLABS and props.get('waterlogged', 'false') == 'false':
return {'bottom': [(0.0, .5)], 'top': [(.5, 1.0)], 'double': [(0.0, 1.0)]}.get(props.get('type'))
if (base.endswith('_stairs') and props.get('shape') == 'straight'
and props.get('half') == 'bottom' and props.get('waterlogged', 'false') == 'false'):
facing = props.get('facing')
if facing not in ('north', 'south', 'east', 'west'):
return None
offset = sub_z if facing in ('north', 'south') else sub_x
if abs(offset - .5) <= 1e-7:
return None
high = offset < .5 if facing in ('north', 'west') else offset > .5
return [(0.0, 1.0 if high else .5)]
return None
def verify_walkable(snapshot, points):
"""Check observed surface samples and their 1.8-block vertical clearance.
`standing_y` is the feet height, not the supporting block Y. Full blocks and
horizontal slabs support a centered player's full footprint. Explicit sub_x
and sub_z fractions additionally sample straight bottom stairs on each side
of their riser. These are point samples, not a full moving-player collision
simulation. Optional ordered `route` points check physical cardinal steps of
at most one block and rises/drops of at most half a block.
"""
failures, routes = [], defaultdict(list)
for number, p in enumerate(points):
if not isinstance(p, dict) or type(p.get('x')) is not int or type(p.get('z')) is not int:
raise ValueError('Walk points require integer x and z')
feet = p.get('standing_y')
if type(feet) not in (int, float) or not math.isfinite(feet) or feet * 2 != round(feet * 2):
raise ValueError('standing_y must be a finite half-block height')
x, z = p['x'], p['z']
sub_x, sub_z = p.get('sub_x', .5), p.get('sub_z', .5)
if any(type(value) not in (int, float) or not math.isfinite(value) or not 0 < value < 1
for value in (sub_x, sub_z)):
raise ValueError('sub_x and sub_z must be finite fractions strictly between 0 and 1')
support_y = math.ceil(feet) - 1
reason = None
try:
support = vertical_shape(snapshot.state(x, support_y, z), sub_x, sub_z)
if support is None:
reason = 'unknown_support_shape'
elif not any(abs(support_y + top - feet) < 1e-8 for _, top in support):
reason = 'missing_support_at_feet'
if reason is None:
for y in range(math.floor(feet), math.ceil(feet + 1.8)):
occupied = vertical_shape(snapshot.state(x, y, z), sub_x, sub_z)
if occupied is None:
reason = 'unknown_clearance_shape'
break
if any(y + bottom < feet + 1.8 and y + top > feet for bottom, top in occupied):
reason = 'blocked_headroom'
break
except KeyError:
reason = 'unobserved_block'
if reason:
failures.append({'index': number, 'x': x, 'z': z, 'sub_x': sub_x, 'sub_z': sub_z,
'standing_y': feet, 'reason': reason})
if 'route' in p:
route = p['route']
if not isinstance(route, str) or not route:
raise ValueError('route must be a nonempty string')
routes[route].append((number, x + sub_x, z + sub_z, feet))
edges = 0
for route, row in routes.items():
for previous, current in zip(row, row[1:]):
edges += 1
dx, dz = abs(previous[1] - current[1]), abs(previous[2] - current[2])
if (dx > 1e-7 and dz > 1e-7) or not 1e-7 < dx + dz <= 1 + 1e-7:
failures.append({'index': current[0], 'route': route, 'reason': 'non_cardinal_route_step'})
elif abs(previous[3] - current[3]) > .5:
failures.append({'index': current[0], 'route': route, 'reason': 'route_step_exceeds_half_block'})
return {'checked_points': len(points), 'checked_route_edges': edges, 'failures': failures,
'passed': not failures, 'scope': snapshot.scope,
'note': 'Observed surface samples and vertical headroom; full cubes, slabs and explicit straight bottom-stair samples. Stair samples are a surface profile, not a full player-width collision simulation. Cached snapshot is not a live re-read.'}
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest='command', required=True)
capture = commands.add_parser('scan', help='Read actual blocks; never edits or loads chunks')
capture.add_argument('--min', type=int, nargs=3, metavar=('X', 'Y', 'Z'))
capture.add_argument('--max', type=int, nargs=3, metavar=('X', 'Y', 'Z'))
capture.add_argument('--columns', type=Path)
capture.add_argument('--out', type=Path, required=True)
capture.add_argument('--resume', action='store_true', help='Explicitly reuse older partial observations')
capture.add_argument('--config', type=Path, default=terrain.ROOT / '.runtime/server/plugins/MinecraftBuilderMCP/config.yml')
verify = commands.add_parser('verify-walk', help='Check surfaces against an observed snapshot')
verify.add_argument('--snapshot', type=Path, required=True)
verify.add_argument('--points', type=Path, required=True, help='JSON {scope:{...},points:[{x,z,standing_y,sub_x?,sub_z?,route?}]}')
verify.add_argument('--report', type=Path, required=True)
args = parser.parse_args()
if args.command == 'scan':
if args.columns:
if args.min or args.max:
parser.error('Use --columns or --min/--max, not both')
cells = column_cells(json.loads(args.columns.read_text()))
else:
if not args.min or not args.max:
parser.error('--min and --max are required without --columns')
cells = box_cells(dict(zip(AXES, args.min)), dict(zip(AXES, args.max)))
result = scan(terrain.Backend(args.config), cells, args.out, args.resume,
progress=lambda p: print(json.dumps(p), flush=True) if p['cells'] % 16 == 0 or p['cells'] == p['total_cells'] else None)
print(json.dumps({'status': 'captured', 'scope': result.scope, 'cells': len(cells),
'voxels': sum(volume(c) for c in cells), 'path': str(args.out.resolve())}))
else:
document = json.loads(args.points.read_text())
result = verify_walkable(load_snapshot(args.snapshot, scope_of(document.get('scope', {}))), document['points'])
args.report.parent.mkdir(parents=True, exist_ok=True)
terrain.save(args.report, result)
print(json.dumps({'passed': result['passed'], 'checked_points': result['checked_points'],
'failures': len(result['failures']), 'report': str(args.report.resolve())}))
if not result['passed']:
raise SystemExit(1)
if __name__ == '__main__':
main()
+19
View File
@@ -0,0 +1,19 @@
# Shacraft layout study
`plan.py` produces a terrain-aware block marking plan and a review image. It reads the approved, big-endian 768 × 768 float height field at `.runtime/terrain-study/natural.f32`; it does not connect to Minecraft or write world blocks.
Run with the terrain study's NumPy / Matplotlib environment:
```sh
.runtime/terrain-study/venv/bin/python scripts/layout-study/plan.py
```
Outputs are `.runtime/layout-study/layout.json` and `layout-preview.png`. The image is a computed planning diagram, not an in-game photograph or a live map. Live world state and safe replacement materials must be checked separately before marking.
The plan retains the Shacraft reference's district order while adapting footprints to the natural valley. A central clock avenue gives spawn a clear destination. The promenade links the station, portals, waterworks, market, gardens and harbor without forcing every trip through spawn. Scenic branches stay subordinate to that main circulation.
The market uses six separate foundations on the calmer southern shoulder. The portal hall is moved north of the lake edge. All building and plaza footprints in the plan lie on dry ground. No route centerline cuts through a building interior by more than a doorway allowance. This is a geometric check, not a human navigation playtest.
JSON coordinates are `[x,z]`, with north in negative Z. Feature polylines and route points are already sampled and rounded. Routes also retain their sparse `waypoints`. `role: bridge` routes have a planned `deck_y` and `abutments` with measured ground height and a future stairs flag. The northeastern bridge needs a substantial east landing stair: its deck is Y86 while that bank is approximately Y69. The three harbor piers and flagship reservation also carry explicit design heights.
Ground contours reserve future buildings; they do not specify large flat platforms. The station and portal hall still span sloped ground, so later architecture should use separate foundations, lower wings and short stairs. The actual terrain, rivers and lake remain the governing geometry. Side trails on mountain shoulders may need stair segments when built.
+188
View File
@@ -0,0 +1,188 @@
"""Design-stage contours and circulation derived from Shacraft's approved height field.
No world access or mutation. Coordinates use north=-Z; fields are planning data only.
"""
from pathlib import Path
import json, math
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.colors import LightSource
from matplotlib.path import Path as MplPath
ROOT=Path(__file__).resolve().parents[2]
OUT=ROOT/'.runtime/layout-study'; OUT.mkdir(parents=True,exist_ok=True)
h=np.fromfile(ROOT/'.runtime/terrain-study/natural.f32',dtype='>f4').reshape(768,768).astype(float)
features=[];routes=[]
colors={'01':'#84d440','02':'#f4d35e','03':'#bc80ef','04':'#40c8e3','05':'#64d9a0','06':'#ef9863','07':'#f5f4e9','08':'#4ca3ff','09':'#f07eaf'}
districts=[
('01','Arrival square',[0,8],'An open hexagonal plaza; preserve the raised grassy crown. Main north axis reveals the clock tower.'),
('02','Clock station',[-18,-119],'A long station hall with projecting clock tower and two end pavilions. Its forecourt faces spawn.'),
('03','Portal concourse',[-201,-156],'A chamfered hall with six separate portal-bay footprints and a sunken-feeling garden approach.'),
('04','Airship harbor',[179,-137],'East-bank terminal with three west-facing piers above the water gorge. One future flagship uses the middle pier.'),
('05','Sky gardens',[207,88],'Three distinct rounded landmarks connected by winding garden paths: greenhouse, winter garden, observatory.'),
('06','Market quarter',[-195,225],'Six small footprints follow the slope around a compact square. Use stepped streets and individual foundations.'),
('07','Arrival viaduct',[8,284],'A long south approach follows the valley ridge; the final viaduct crosses the merging river branches.'),
('08','Lake waterworks',[-135,43],'A compact pumping house above the east lake shore and a low over-water promenade.'),
('09','Scenic overlooks',[48,-180],'Small optional lookouts frame the valley, lake and arrival route; no mountain flattening.')]
districts=[dict(id=i,name=n,label=p,color=colors[i],intent=t) for i,n,p,t in districts]
def closed(points): return points+[points[0]] if points[0]!=points[-1] else points
def add(i,d,n,p,role='building',**kw):
f=dict(id=i,district=d,name=n,type='polyline' if role in ('detail','promenade','pier','bridge','axis') else 'polygon',points=p,color=colors[d],role=role,**kw)
features.append(f);return f
def ellipse(cx,cz,rx,rz=None,n=72,angle=0):
rz=rx if rz is None else rz;a=math.radians(angle)
return closed([[round(cx+math.cos(t)*rx*math.cos(a)-math.sin(t)*rz*math.sin(a)),round(cz+math.cos(t)*rx*math.sin(a)+math.sin(t)*rz*math.cos(a))]for t in np.linspace(0,2*math.pi,n,endpoint=False)])
def rect(cx,cz,w,d,angle=0):
a=math.radians(angle)
return closed([[round(cx+x*math.cos(a)-z*math.sin(a)),round(cz+x*math.sin(a)+z*math.cos(a))]for x,z in[(-w/2,-d/2),(w/2,-d/2),(w/2,d/2),(-w/2,d/2)]])
def smooth(points):
p=np.array([points[0]]+points+[points[-1]],float);out=[]
for k in range(1,len(p)-2):
a,b,c,d=p[k-1:k+3];n=max(2,int(np.linalg.norm(c-b)))
for t in np.linspace(0,1,n,endpoint=False):
q=.5*((2*b)+(-a+c)*t+(2*a-5*b+4*c-d)*t*t+(-a+3*b-3*c+d)*t*t*t)
q=[round(float(q[0])),round(float(q[1]))]
if not out or q!=out[-1]:out.append(q)
out.append(points[-1]);return out
def route(i,name,points,width=7,role='main',**kw):
r=dict(id=i,name=name,points=smooth(points),waypoints=points,width=width,role=role,**kw);routes.append(r);return r
add('arrival-hex','01','Arrival square',closed([[0,-37],[39,-15],[43,31],[0,57],[-43,31],[-39,-15]]),'plaza')
add('spawn-medallion','01','Shacraft medallion reserve',ellipse(0,9,16,n=36),'plaza')
add('clock-station','02','Clock station and tower',closed([[-74,-139],[-51,-139],[-51,-145],[16,-145],[16,-139],[40,-139],[40,-98],[13,-98],[13,-83],[-25,-83],[-25,-98],[-74,-98]]))
add('station-clock-base','02','Clock tower base',rect(-6,-105,18,18),'detail')
for cx in[-63,29]:add('station-pavilion-'+str(cx),'02','End pavilion',rect(cx,-119,16,30),'detail')
add('station-forecourt','02','Station forecourt',ellipse(-5,-65,36,15,n=48),'plaza')
add('portal-hall','03','Portal concourse',closed([[-227,-163],[-161,-163],[-152,-154],[-152,-122],[-161,-113],[-227,-113],[-236,-122],[-236,-154]]))
for k,(cx,cz) in enumerate([(x,z)for z in[-151,-125]for x in[-218,-194,-170]],1):add('portal-bay-'+str(k),'03','Portal bay '+str(k),rect(cx,cz,13,8),'detail',number=k)
add('portal-forecourt','03','Portal garden court',ellipse(-159,-93,17,12,n=48),'plaza')
add('harbor-terminal','04','Airship terminal',closed([[169,-170],[191,-170],[199,-162],[199,-103],[191,-95],[169,-95],[161,-103],[161,-162]]))
for k,z in enumerate([-158,-132,-106],1):
add('airship-pier-'+str(k),'04','Airship pier '+str(k),closed([[161,z-4],[107,z-4],[103,z],[107,z+4],[161,z+4]]),'pier',deck_y=79,number=k)
add('flagship-reserve','04','Flagship mooring reserve',ellipse(78,-132,29,10,n=48),'detail',deck_y=92)
for i,(x,z,rx,rz,n) in enumerate([(185,23,25,25,'Palm glasshouse'),(211,94,22,18,'Winter garden'),(233,152,15,15,'Observatory')],1):
add('garden-'+str(i),'05',n,ellipse(x,z,rx,rz), 'building')
add('garden-court-'+str(i),'05',n+' surrounding walk',ellipse(x,z,rx+7,rz+7),'plaza')
add('market-square','06','Market square',ellipse(-195,225,22,25,n=48),'plaza')
for i,(x,z,w,d,a,n) in enumerate([(-219,188,24,18,-8,'Bakery'),(-177,195,24,18,7,'Crafts hall'),(-162,231,17,26,8,'Tea house'),(-178,272,22,18,-8,'Guild shop'),(-214,272,23,17,8,'Workshop'),(-233,239,22,26,-5,'Guild hall')],1):
add('market-'+str(i),'06',n,rect(x,z,w,d,a))
add('waterworks-pump','08','Pumping house',rect(-135,43,14,18,-15))
add('waterworks-lookout','08','Waterworks lookout',ellipse(-135,69,10,n=32),'plaza')
shore=[]
for z in range(-69,76,3):
wet=np.where(h[z+384,:384]<48)[0]-384
if len(wet):shore.append([int(wet[-1]-3),z])
add('lake-boardwalk','08','Lake boardwalk alignment',smooth(shore),'promenade',deck_y=51)
for i,(x,z,r,n) in enumerate([(43,-173,8,'North clock-view belvedere'),(-130,-188,7,'Portal ridge overlook'),(229,-76,8,'Harbor overlook'),(266,110,7,'Garden lookout'),(-258,258,7,'Valley approach lookout')],1):
add('overlook-'+str(i),'09',n,ellipse(x,z,r,n=32),'plaza')
# Pull the portal footprint north onto its drier shoulder; retain all six bays.
for f in features:
if f['id']=='portal-hall' or f['id'].startswith('portal-bay-'):
f['points']=[[x-7,round(-156+(z+138)*.8)] for x,z in f['points']]
# Direct spawn routes make the principal destinations easy to find.
route('station-axis','Clock avenue',[[0,-37],[-4,-48],[-5,-65],[-6,-83]],9)
route('portal-radial','Portal approach',[[-35,-14],[-67,-43],[-107,-65],[-135,-75],[-159,-81]],7)
route('portal-forecourt-link','Portal court entrance',[[-159,-105],[-154,-120],[-164,-139]],5,'secondary')
route('lake-radial','Lake walk',[[-42,18],[-77,27],[-104,38],[-126,40]],5,'secondary')
route('east-radial','Garden approach',[[42,16],[65,10],[82,8]],7)
route('south-axis','Arrival avenue',[[0,57],[3,97],[-5,143],[4,189],[8,237],[7,281],[5,321]],9)
# A district promenade closes two large loops and bypasses spawn.
route('ring-northwest','Station to portals',[[-40,-65],[-77,-76],[-116,-89],[-145,-105],[-152,-122],[-164,-139]],7)
route('ring-west','West inner promenade',[[-147,-111],[-125,-80],[-118,-41],[-110,4],[-115,47],[-113,88],[-127,126]],7)
route('ring-market-north','Market north street',[[-213,128],[-218,148],[-205,171],[-193,182],[-195,200]],7)
route('ring-market-south','Market south street',[[-195,250],[-187,253],[-170,252],[-145,242]],7)
route('ring-south','South inner promenade',[[-72,242],[-36,238],[8,237],[31,218],[46,194],[59,166]],7)
route('ring-garden-south','Garden south promenade',[[143,166],[172,179],[203,172],[218,157]],7)
route('ring-gardens','Garden promenade',[[218,137],[204,117],[186,109],[176,88],[184,65],[201,46]],7)
route('ring-harbor','Harbor garden promenade',[[185,-9],[197,-43],[192,-72],[181,-95]],7)
route('ring-northeast','Station to east bridge',[[40,-95],[60,-91],[82,-78]],7)
route('harbor-bridge-landing','Harbor bridge landing',[[155,-81],[171,-83],[180,-95]],7)
route('garden-bridge-landing','Garden bridge landing',[[151,8],[157,16],[160,23]],7)
route('market-shop-street','Market north street',[[-207,192],[-205,198],[-203,202]],5,'secondary')
route('market-south-shop-street','Market south shops',[[-187,253],[-178,259],[-178,262]],5,'secondary')
route('market-workshop-street','Workshop approach',[[-198,250],[-207,254],[-214,262]],5,'secondary')
route('market-guild-street','Guild approach',[[-216,231],[-220,234],[-222,236]],5,'secondary')
route('market-teahouse-street','Tea house approach',[[-173,229],[-171,229]],5,'secondary')
route('market-crafts-street','Crafts approach',[[-184,203],[-179,206]],5,'secondary')
route('waterworks-connector','Waterworks approach',[[-114,44],[-122,44],[-128,43]],5,'secondary')
route('waterworks-lookout-path','Waterworks viewing path',[[-135,52],[-135,59]],3,'secondary')
# Bridges are measured separately. Grade is a later construction task, not terrain flattening.
for i,n,a,b,w in [
('bridge-northeast','Clock / harbor bridge',[82,-78],[155,-81],7),
('bridge-east','Spawn / garden bridge',[82,8],[151,8],7),
('bridge-southeast','Garden / south bridge',[59,166],[143,166],7),
('bridge-market-north','Market / lake bridge',[-127,126],[-213,128],7),
('bridge-market-south','Market / arrival bridge',[-145,242],[-72,242],7),
('arrival-viaduct','Arrival viaduct',[5,321],[5,383],11)]:
pts=np.linspace(a,b,int(np.linalg.norm(np.array(b)-a))+1).round().astype(int)
heights=h[pts[:,1]+384,pts[:,0]+384]
deck=int(math.ceil(max(heights[0],heights[-1])))+2
if i=='arrival-viaduct':deck=max(deck,65)
r=route(i,n,[a,b],w,'bridge',deck_y=deck,minimum_ground=float(heights.min()),endpoint_ground=[float(heights[0]),float(heights[-1])]);
# Edge lines are appropriate for construction marking; they are not a complete bridge.
r['intent']='Mark both parapet alignments and abutments; preserve the river and banks.'
r['abutments']=[dict(point=q,ground_y=round(float(y),1),deck_y=deck,future_stairs=(deck-math.floor(y)>4),rise=deck-math.floor(y)) for q,y in zip([a,b],[heights[0],heights[-1]])]
# Scenic branches never substitute for main circulation.
for i,n,p in[
('north-overlook-path','North overlook',[[40,-128],[58,-140],[62,-161],[50,-171]]),
('portal-overlook-path','Portal ridge path',[[-159,-151],[-136,-158],[-123,-172],[-129,-181]]),
('harbor-overlook-path','Harbor lookout trail',[[196,-80],[213,-75],[221,-76]]),
('garden-overlook-path','Garden lookout trail',[[232,96],[249,99],[259,107]]),
('south-overlook-path','Valley lookout trail',[[-218,236],[-224,219],[-248,223],[-263,240],[-264,251]])]:route(i,n,p,3,'secondary')
# Geometric checks on the immutable approved field; live blocks must still be checked before writes.
zz,xx=np.mgrid[-384:384,-384:384]
for f in features:
pts=np.array(f['points']);sample=h[np.clip(pts[:,1]+384,0,767),np.clip(pts[:,0]+384,0,767)]
if f['type']=='polygon':
x0,z0=pts.min(axis=0);x1,z1=pts.max(axis=0);px,pz=np.meshgrid(np.arange(x0,x1+1),np.arange(z0,z1+1))
mask=MplPath(pts).contains_points(np.column_stack((px.flat,pz.flat)),radius=.01).reshape(px.shape)
sample=h[z0+384:z1+385,x0+384:x1+385][mask]
f['ground']={'min':round(float(sample.min()),2),'max':round(float(sample.max()),2),'water_samples':int((sample<48).sum()),'samples':len(sample)}
for r in routes:
pts=np.array(r['points']);v=h[pts[:,1]+384,pts[:,0]+384]
ds=np.linalg.norm(np.diff(pts,axis=0),axis=1);grade=np.abs(np.diff(v))/np.maximum(ds,.001)
r['analysis']={'length':round(float(ds.sum()),1),'ground_min':round(float(v.min()),1),'ground_max':round(float(v.max()),1),'p95_raw_grade':round(float(np.percentile(grade,95)),2),'water_samples':int((v<48).sum())}
plan={'schema':'shacraft-layout-study-v1','world':'shacraft_lobby_v2','bounds':{'min_x':-384,'max_x':383,'min_z':-384,'max_z':383},'water_y':48,'status':'offline design; not a world mutation or live snapshot','districts':districts,'features':features,'routes':routes,'construction_notes':[
'Colored outlines are building reservations, not instructions to level their entire bounding boxes.',
'Walks remain aligned to terrain; steep local runs need short stairs during the later building phase.',
'Main avenues 9 blocks, district ring 7, side paths 5, scenic trails 3; marker widths can be thinner than final clear widths.',
'Keep protected water intact. Bridges have a separate planned deck Y above their endpoint terrain.',
'The natural western lake bank is too steep for a main ring street. The main route uses the calm inner/east lake shoulder; the low over-water boardwalk is secondary.',
'No minigame arenas. Six portal bays, three harbor piers, one future flagship reservation.'
]}
(OUT/'layout.json').write_text(json.dumps(plan,indent=2)+'\n')
# Render a design review from data, not a Minecraft screenshot.
dz,dx=np.gradient(h);rock=np.clip((np.hypot(dx,dz)-.55)/1.5,0,1)[...,None]
rgb=np.array([.32,.40,.27])*(1-rock)+np.array([.49,.49,.46])*rock
rgb=LightSource(315,42).shade_rgb(rgb,h,vert_exag=1,blend_mode='soft');rgb=np.where((h<48)[...,None],np.array([.13,.28,.35]),rgb)
fig,ax=plt.subplots(figsize=(14,14),facecolor='#151b18');ax.set_facecolor('#151b18')
ax.imshow(rgb,extent=(-384,384,384,-384))
for r in routes:
p=np.array(r['points']);lw=2.2 if r['role']!='secondary' else 1
ax.plot(p[:,0],p[:,1],color='#f7f2de' if r['role']!='bridge' else '#ffffff',lw=lw,alpha=.92,linestyle='-' if r['role']!='bridge' else '--')
for f in features:
p=np.array(f['points']);ax.plot(p[:,0],p[:,1],color=f['color'],lw=2 if f['role'] not in ('detail','promenade') else 1.25)
for d in districts:
if d['id']=='09':continue
x,z=d['label'];ax.text(x,z,d['id'],color='#111914',ha='center',va='center',fontsize=12,fontweight='bold',bbox=dict(boxstyle='circle,pad=.35',fc=d['color'],ec='#111914',lw=1))
ax.set(xlim=(-325,310),ylim=(384,-255),xlabel='X / blocks',ylabel='Z / blocks; north up')
ax.set_title('SHACRAFT / terrain-adapted lobby layout',loc='left',color='#f5f3ea',fontsize=19,pad=22)
ax.tick_params(colors='#b5c0b8');ax.xaxis.label.set_color('#b5c0b8');ax.yaxis.label.set_color('#b5c0b8')
for i,d in enumerate(districts):
fig.text(.085+(i%3)*.302,.065-(i//3)*.021,d['id']+' '+d['name'],color=d['color'],fontsize=11)
fig.text(.085,.009,'DESIGN STUDY • Exact approved height field; outlines not yet placed. White: routes / dashed: future bridge spans.',color='#b5c0b8',fontsize=9)
fig.subplots_adjust(left=.06,right=.985,top=.945,bottom=.11)
fig.savefig(OUT/'layout-preview.png',dpi=140);plt.close(fig)
print(json.dumps({'features':len(features),'routes':len(routes),'path_length':round(sum(r['analysis']['length']for r in routes)), 'output':str(OUT)}))
print('BUILDING HEIGHT SPANS')
for f in features:
if f['role']=='building':print(f['id'],f['ground'])
print('BRIDGES')
for r in routes:
if r['role']=='bridge':print(r['id'],r['deck_y'],r['endpoint_ground'],r['analysis'])
+326
View File
@@ -0,0 +1,326 @@
#!/usr/bin/env python3
"""Place an explicit block layout with checked snapshots, resumable receipts, and guarded undo."""
import argparse
from collections import Counter, defaultdict
import fcntl
import hashlib
import importlib.util
import json
from pathlib import Path
import re
import sys
import time
spec = importlib.util.spec_from_file_location('layout_terrain', Path(__file__).with_name('terrain.py'))
terrain = importlib.util.module_from_spec(spec)
spec.loader.exec_module(terrain)
ROOT = terrain.ROOT
MAX_BLOCKS = 4096
MAX_OPERATIONS = 256
MAX_READ_CELLS = 32
SCOPE_KEYS = ('project_id', 'world_id', 'world_epoch')
FAILED = ('conflict', 'cancelled', 'failed', 'recovery_required')
STATE = re.compile(r'minecraft:[a-z0-9_]+(?:\[[a-z0-9_=,]+\])?\Z')
def position(block):
return tuple(block[axis] for axis in ('x', 'y', 'z'))
def coordinates(at):
return dict(zip(('x', 'y', 'z'), at))
def normalize(value):
if not isinstance(value, dict) or type(value.get('version')) is not int or value['version'] != 1:
raise ValueError('Layout version must be 1')
scope = value.get('scope', {})
if not isinstance(scope, dict) or any(not isinstance(scope.get(k), str) or not scope[k] for k in SCOPE_KEYS):
raise ValueError('Layout requires project_id, world_id, and world_epoch')
source = value.get('blocks')
if not isinstance(source, list) or not 1 <= len(source) <= 1_000_000:
raise ValueError('Layout must contain 1..1,000,000 explicit blocks')
blocks, seen = [], set()
for raw in source:
if not isinstance(raw, dict):
raise ValueError('Every block must be an object')
if any(type(raw.get(a)) is not int or not -2**31 <= raw[a] < 2**31 for a in ('x', 'y', 'z')):
raise ValueError('Block coordinates must be signed 32-bit integers')
at = position(raw)
if at in seen:
raise ValueError(f'Duplicate block position: {at}')
seen.add(at)
block, expected = raw.get('block'), raw.get('expected', 'minecraft:air')
if any(not isinstance(s, str) or len(s) > 512 or not STATE.fullmatch(s) for s in (block, expected)):
raise ValueError(f'Invalid block state at {at}; use full minecraft: names')
blocks.append({**coordinates(at), 'block': block, 'expected': expected})
blocks.sort(key=position)
return {'version': 1, 'scope': {k: scope[k] for k in SCOPE_KEYS}, 'blocks': blocks}
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
def read_cell(block):
return (block['x'] // 16, block['y'] // 16, block['z'] // 16)
def bounds(blocks):
return {'min': {a: min(b[a] for b in blocks) for a in ('x', 'y', 'z')},
'max': {a: max(b[a] for b in blocks) for a in ('x', 'y', 'z')}}
def volume(box):
return (box['max']['x'] - box['min']['x'] + 1) * (box['max']['y'] - box['min']['y'] + 1) * (box['max']['z'] - box['min']['z'] + 1)
def read_groups(blocks):
cells = defaultdict(list)
for block in blocks:
cells[read_cell(block)].append(block)
return [cells[key] for key in sorted(cells)]
def compressed_runs(blocks):
"""Runs never cross a 16³ read cell, so every inspection is bounded to 4096 voxels."""
rows = defaultdict(list)
for block in blocks:
rows[(*read_cell(block), block['y'], block['z'], block['block'])].append(block)
for key in sorted(rows):
row = sorted(rows[key], key=lambda b: b['x'])
run = []
for block in row:
if run and block['x'] != run[-1]['x'] + 1:
yield run
run = []
run.append(block)
if run:
yield run
def make_batches(blocks):
batches, batch, cells = [], [], set()
for run in compressed_runs(blocks):
cell = read_cell(run[0])
if batch and (len(batch) >= MAX_OPERATIONS or sum(len(r) for r in batch) + len(run) > MAX_BLOCKS
or len(cells | {cell}) > MAX_READ_CELLS):
batches.append(batch)
batch, cells = [], set()
batch.append(run)
cells.add(cell)
if batch:
batches.append(batch)
result = []
for index, runs in enumerate(batches):
selected = [b for run in runs for b in run]
operations = [{'type': 'box', 'min': coordinates(position(run[0])),
'max': coordinates(position(run[-1])), 'block': run[0]['block']} for run in runs]
result.append({'index': index, 'blocks': selected, 'recipe': {'version': 1, 'operations': operations}})
return result
def check_context(context, layout, require_checked=True):
if {key: context.get(key) for key in SCOPE_KEYS} != layout['scope']:
raise RuntimeError('Layout belongs to another project/world/epoch')
if require_checked and context.get('checked_expected_blocks') is not True:
raise RuntimeError('Server lacks atomic checked_expected_blocks; update the plugin before placing this layout')
area = context.get('region', {})
for block in layout['blocks']:
if any(not area.get('min', {}).get(a, 1) <= block[a] <= area.get('max', {}).get(a, 0) for a in ('x', 'y', 'z')):
raise RuntimeError(f'Layout exceeds selected project area at {position(block)}')
def inspect_states(backend, blocks, epoch):
states = {}
for group in read_groups(blocks):
box = bounds(group)
result = backend.call('region_inspect', **box, detail='blocks')
if result.get('world_epoch') != epoch or result.get('truncated') is not False:
raise RuntimeError('Inspection was truncated or returned a different world epoch')
found = {}
for item in result.get('blocks', []):
at = position(item['pos'])
if at in found:
raise RuntimeError('Inspection contained duplicate positions')
found[at] = item['state']
if len(found) != volume(box):
raise RuntimeError('Inspection did not return the complete bounded region')
for block in group:
at = position(block)
if at not in found:
raise RuntimeError(f'Inspection omitted {at}')
states[at] = found[at]
return states
def verify(backend, blocks, epoch, field):
states = inspect_states(backend, blocks, epoch)
mismatches = [(position(b), b[field], states[position(b)]) for b in blocks if states[position(b)] != b[field]]
if mismatches:
at, expected, actual = mismatches[0]
raise RuntimeError(f'Block mismatch at {at}: expected {expected}, found {actual} ({len(mismatches)} mismatches); no blind overwrite')
return len(blocks)
def load_or_create(path, layout, batches):
expected_digest = digest(layout)
snapshot = path.with_suffix(path.suffix + '.input.json')
if path.exists():
manifest = json.loads(path.read_text())
if (manifest.get('version') != 1 or manifest.get('planning_version') != 1
or manifest.get('scope') != layout['scope'] or manifest.get('input_digest') != expected_digest):
raise RuntimeError('Manifest layout digest, planning version, or project/world/epoch differs; do not reuse it')
if not snapshot.exists() or digest(normalize(json.loads(snapshot.read_text()))) != expected_digest:
raise RuntimeError('Saved layout input is missing or changed')
if len(manifest.get('batches', [])) != len(batches):
raise RuntimeError('Saved batch count differs from deterministic planner')
for saved, planned in zip(manifest['batches'], batches):
if saved.get('index') != planned['index'] or saved.get('digest') != digest(planned):
raise RuntimeError('Saved batch differs from deterministic planner')
return manifest
if snapshot.exists() and digest(normalize(json.loads(snapshot.read_text()))) != expected_digest:
raise RuntimeError('An orphaned layout input differs; choose a new manifest path')
terrain.save(snapshot, layout)
manifest = {'version': 1, 'planning_version': 1, 'scope': layout['scope'],
'input_digest': expected_digest, 'blocks': len(layout['blocks']),
'batches': [{'index': b['index'], 'digest': digest(b), 'blocks': len(b['blocks'])} for b in batches]}
terrain.save(path, manifest)
return manifest
def apply_layout(backend, layout, path, progress=print):
check_context(backend.call('project_context'), layout)
batches = make_batches(layout['blocks'])
manifest = load_or_create(path, layout, batches)
if manifest.get('undo_started') or manifest.get('undone') or any('undo' in e for e in manifest['batches']):
raise RuntimeError('Layout has begun undo; finish undo and use a new manifest for new work')
epoch = layout['scope']['world_epoch']
for batch, entry in zip(batches, manifest['batches']):
if entry.get('status') in FAILED:
raise RuntimeError(f"Batch {entry['index']} stopped on {entry['status']}; inspect or undo it instead of creating a new plan")
if entry.get('status') == 'applied':
verify(backend, batch['blocks'], epoch, 'block')
continue
# An unknown apply may already have changed the world: resolve its same stable key first.
if 'idempotency_key' not in entry and 'operation_id' not in entry:
verify(backend, batch['blocks'], epoch, 'expected')
if 'plan_id' not in entry:
prepared = backend.call('build_prepare', recipe=batch['recipe'], expected_blocks=[
{'pos': coordinates(position(b)), 'state': b['expected']} for b in batch['blocks']])
entry.update(prepared)
terrain.save(path, manifest)
terrain.apply_plan(backend, entry, manifest, path)
verify(backend, batch['blocks'], epoch, 'block')
entry['verified_at'] = int(time.time())
terrain.save(path, manifest)
progress(json.dumps({'batch': entry['index'] + 1, 'batches': len(batches), 'status': 'verified',
'written': entry['written'], 'operation_id': entry['operation_id']}))
manifest['completed'] = True
terrain.save(path, manifest)
return {'status': 'completed', 'blocks': len(layout['blocks']), 'batches': len(batches), 'manifest': str(path)}
def undo_layout(backend, path, progress=print):
snapshot = path.with_suffix(path.suffix + '.input.json')
if not path.exists() or not snapshot.exists():
raise RuntimeError('Manifest or saved layout input not found')
layout = normalize(json.loads(snapshot.read_text()))
check_context(backend.call('project_context'), layout, require_checked=False)
batches = make_batches(layout['blocks'])
manifest = load_or_create(path, layout, batches)
for entry in manifest['batches']:
if 'idempotency_key' in entry and 'operation_id' not in entry:
raise RuntimeError('Uncertain apply response. Resume apply with the same manifest first; undo will not start an unconfirmed write')
manifest['undo_started'] = True
terrain.save(path, manifest)
for batch, entry in reversed(list(zip(batches, manifest['batches']))):
if 'operation_id' not in entry:
continue
source = terrain.finish(backend, entry['operation_id'])
if source['status'] == 'recovery_required':
raise RuntimeError('Interrupted server operation requires recovery review before undo')
if not source['written']:
continue
if 'undo' not in entry:
entry['undo'] = backend.call('operation_undo_prepare', operation_id=entry['operation_id'])
terrain.save(path, manifest)
if entry['undo'].get('status') in FAILED:
raise RuntimeError(f"Undo stopped on {entry['undo']['status']}; inspect conflicts before taking further action")
terrain.apply_plan(backend, entry['undo'], manifest, path)
# Only a fully applied source has receipts for every changed position. A guarded partial
# undo intentionally leaves conflicting manual edits alone; the server verifies its receipts.
if source['status'] == 'applied':
verify(backend, batch['blocks'], layout['scope']['world_epoch'], 'expected')
entry['undo']['verified_at'] = int(time.time())
else:
entry['undo']['verification'] = 'server_receipts_only_for_partial_source'
terrain.save(path, manifest)
progress(json.dumps({'batch': entry['index'] + 1, 'status': 'undone', 'written': entry['undo']['written']}))
manifest['undone'] = True
terrain.save(path, manifest)
return {'status': 'undone', 'manifest': str(path)}
def report(layout):
batches = make_batches(layout['blocks'])
boxes = [bounds(group) for b in batches for group in read_groups(b['blocks'])]
return {'status': 'offline_report', 'scope': layout['scope'], 'input_digest': digest(layout),
'blocks': len(layout['blocks']), 'batches': len(batches), 'bounds': bounds(layout['blocks']),
'materials': dict(sorted(Counter(b['block'] for b in layout['blocks']).items())),
'inspection_requests_per_pass': len(boxes), 'inspection_voxels_per_pass': sum(map(volume, boxes)),
'max_inspection_volume': max(map(volume, boxes)),
'max_batch_blocks': max(len(b['blocks']) for b in batches),
'max_batch_operations': max(len(b['recipe']['operations']) for b in batches)}
def main():
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest='command', required=True)
p = sub.add_parser('report', help='Report deterministic batching offline; no server required')
p.add_argument('layout', type=Path)
descriptions = {'prepare': 'Save immutable input and check current blocks without editing the world',
'apply': 'Place or resume the layout through checked, journalled RPC',
'undo': 'Undo recorded layout operations in reverse order, preserving later edits'}
for name, description in descriptions.items():
p = sub.add_parser(name, help=description)
if name != 'undo':
p.add_argument('layout', type=Path)
p.add_argument('--manifest', type=Path, required=True)
p.add_argument('--config', type=Path, default=ROOT / '.runtime/server/plugins/MinecraftBuilderMCP/config.yml')
p.add_argument('--console', action='store_true', help='Only for an explicitly configured isolated fixture')
if name != 'prepare':
p.add_argument('--execute', action='store_true', required=True, help='Explicitly perform this world edit')
args = parser.parse_args()
if args.command == 'report':
print(json.dumps(report(normalize(json.loads(args.layout.read_text()))), indent=2))
return
path = args.manifest.resolve()
path.parent.mkdir(parents=True, exist_ok=True)
with path.with_suffix(path.suffix + '.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
backend = terrain.Backend(args.config, args.console)
if args.command == 'undo':
result = undo_layout(backend, path, progress=lambda s: print(s, flush=True))
else:
layout = normalize(json.loads(args.layout.read_text()))
if args.command == 'prepare':
check_context(backend.call('project_context'), layout)
batches = make_batches(layout['blocks'])
manifest = load_or_create(path, layout, batches)
if any('plan_id' in e for e in manifest['batches']):
raise RuntimeError('This manifest already has plans; use apply to resume, or report for an offline summary')
for batch in batches:
verify(backend, batch['blocks'], layout['scope']['world_epoch'], 'expected')
result = {**report(layout), 'status': 'prepared_input', 'manifest': str(path)}
else:
result = apply_layout(backend, layout, path, progress=lambda s: print(s, flush=True))
print(json.dumps(result, indent=2))
if __name__ == '__main__':
try:
main()
except (RuntimeError, ValueError, OSError) as error:
print(f'Layout stopped: {error}', file=sys.stderr)
sys.exit(1)
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Compile concealed station lighting against an explicit observed snapshot.
No world I/O. Each fitting replaces full cubes only: brown glass forms a flush
floor tile or a recessed ceiling lens, with glowstone hidden directly behind it.
The caller applies and independently verifies the resulting checked recipe.
"""
import argparse
from collections import Counter
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
STAGE = ROOT / ".runtime/station-stage07"
GLOW = "minecraft:glowstone"
LENS = "minecraft:brown_stained_glass"
WOOD = "minecraft:spruce_planks"
CREAM = "minecraft:smooth_sandstone"
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("station_lighting_survey", ROOT / "scripts/foundation-survey.py")
def compile_lighting(before, station):
if station["scope"] != before.scope:
raise ValueError("Station metadata and observed snapshot have different scopes")
footprint = {tuple(p) for p in station["footprint"]}
inside = {(x, z) for x, z in footprint
if all((x + dx, z + dz) in footprint
for dx in range(-2, 3) for dz in range(-2, 3))}
targets = {feet: {(t["x"] + dx, t["z"] + dz)
for t in station["interior"]["navigation"][str(feet)]["named_targets"]
for dx in range(-1, 2) for dz in range(-1, 2)}
for feet in (99, 113)}
# Keep both cabin landings, controls, doorway and immediate approach unchanged.
lift_exclusion = {(x, z) for x in range(-11, 0) for z in range(-125, -112)}
changes = {}
fixtures = []
def full(state):
return state.split("[", 1)[0].removeprefix("minecraft:") in survey.FULL
def put(x, y, z, value, group):
if (x, z) not in inside or not 97 <= y <= 124:
raise ValueError(f"Lighting write outside authorized volume: {(x, y, z)}")
old = before.state(x, y, z)
if not full(old) or not full(value):
raise ValueError(f"Lighting may replace full cubes only: {(x, y, z)} {old}")
if old == value:
return
p = (x, y, z)
if p in changes and changes[p]["block"] != value:
raise ValueError(f"Lighting layers disagree at {p}")
changes[p] = {"x": x, "y": y, "z": z, "expected": old,
"block": value, "group": group}
for feet, ceiling in ((99, 110), (113, 123)):
public = {(p["x"], p["z"]) for p in station["interior"]["walk_points_by_floor"][str(feet)]}
public &= inside
candidates = {"floor": set(), "ceiling": set()}
for x, z in sorted(public - lift_exclusion):
# Existing flooring mosaics and green/brass bands are not recoloured.
if (x, z) not in targets[feet] and before.state(x, feet - 1, z) == CREAM:
neighborhood = {(x + dx, z + dz) for dx in range(-1, 2) for dz in range(-1, 2)}
if neighborhood <= public and all(
before.state(xx, y, zz) in survey.AIR
for xx, zz in neighborhood for y in range(feet, feet + 4)
) and full(before.state(x, feet - 2, z)):
candidates["floor"].add((x, z))
# Plain spruce cells only: beams, chains, chandeliers, panels and lift
# geometry are excluded by the observed material and air-column tests.
if (before.state(x, ceiling, z) == WOOD
and full(before.state(x, ceiling + 1, z))
and all(before.state(x, y, z) in survey.AIR for y in range(feet, ceiling))):
candidates["ceiling"].add((x, z))
for kind in ("floor", "ceiling"):
chosen = set()
# A common architectural grid; a maximum two-block adjustment avoids
# a column, mosaic or ceiling beam without creating dense bright rows.
for gx in range(-69, 40, 9):
for gz in range(-139, -83, 9):
nearby = [(x, z) for x in range(gx - 2, gx + 3)
for z in range(gz - 2, gz + 3)
if (x, z) in candidates[kind]]
nearby.sort(key=lambda p: ((p[0] - gx) ** 2 + (p[1] - gz) ** 2,
abs(p[0] - gx) + abs(p[1] - gz), p))
selected = next((p for p in nearby if all(
(p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2 >= 36 for q in chosen)), None)
if selected is None:
continue
x, z = selected
chosen.add(selected)
lens_y, emit_y = (feet - 1, feet - 2) if kind == "floor" else (ceiling, ceiling + 1)
group = f"station-lighting:{feet}:{kind}"
put(x, lens_y, z, LENS, group)
put(x, emit_y, z, GLOW, group)
fixtures.append({"floor_walk_y": feet, "kind": kind,
"lens": [x, lens_y, z], "emitter": [x, emit_y, z],
"grid_anchor_xz": [gx, gz]})
recipe = {"version": 1, "scope": before.scope,
"blocks": [changes[p] for p in sorted(changes)]}
metadata = {
"version": 1, "scope": before.scope, "world_writes": 0,
"status": "checked lighting candidate; live application and visual review pending",
"style": "Sparse warm floor tiles and small concealed ceiling lenses on a nine-block grid",
"grid_spacing": 9, "maximum_anchor_adjustment": 2,
"minimum_same_layer_fixture_distance": 6,
"owned_y": [97, 124], "perimeter_setback": 2,
"changed_states": len(changes), "fixtures": fixtures,
"fixture_counts": dict(sorted(Counter(f"{f['floor_walk_y']}:{f['kind']}" for f in fixtures).items())),
"changed_materials": dict(Counter(row["block"] for row in changes.values())),
"checks": {"only_full_cube_replacements": True, "no_new_collision_obstructions": True,
"existing_mosaic_bands_preserved": True, "lift_landings_and_doorway_excluded": True,
"floor_fixtures_outside_named_target_neighborhoods": True,
"floor_fixtures_have_clear_three_by_three_surrounds": True,
"ceiling_lenses_replace_only_plain_spruce": True,
"decorative_furniture_and_diorama_preserved": True},
"limits": "Fixture geometry does not predict the client's final rendered brightness. Review actual Minecraft images after applying and waiting for lighting updates.",
}
return recipe, metadata
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--before", type=Path, default=STAGE / "after.json.gz")
parser.add_argument("--station-metadata", type=Path, default=STAGE / "compiled/station.metadata.json")
parser.add_argument("--output", type=Path, default=STAGE / "lighting")
args = parser.parse_args()
recipe, metadata = compile_lighting(survey.load_snapshot(args.before), json.loads(args.station_metadata.read_text()))
metadata["compiled_at_utc"] = datetime.now(timezone.utc).isoformat()
metadata["inputs"] = {str(path): hashlib.sha256(path.read_bytes()).hexdigest()
for path in (args.before, args.station_metadata, Path(__file__))}
args.output.mkdir(parents=True, exist_ok=True)
for name, doc in (("lighting.json", recipe), ("lighting.metadata.json", metadata)):
destination = args.output / name
if destination.exists():
raise FileExistsError(f"Preserve the previous candidate: {destination}")
destination.write_text(json.dumps(doc, indent=2) + "\n")
print(json.dumps({"recipe": str(args.output / "lighting.json"), "changed_states": len(recipe["blocks"]),
"fixture_counts": metadata["fixture_counts"]}))
if __name__ == "__main__":
main()
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Compile the Shacraft spatial study into checked, reversible survey blocks.
This only writes a desired-block document. scripts/layout.py performs live edits.
Ground lines replace one observed surface block; they never level terrain.
"""
import argparse
from collections import Counter
import json
import math
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
COLORS = {'01': 'lime', '02': 'yellow', '03': 'purple', '04': 'cyan',
'05': 'red', '06': 'orange', '07': 'white', '08': 'blue', '09': 'pink'}
RGB = {'lime':'#98d84d','yellow':'#f6d34a','purple':'#9460ce','cyan':'#23b6b6',
'green':'#527c31','orange':'#f78d27','white':'#f0f0e6','blue':'#4a69d8',
'pink':'#f394b5','black':'#26282d','light_gray':'#a4aaa6','gray':'#545c61',
'red':'#e3544b','light_blue':'#68c8ec'}
FONT = {
'0':['111','101','101','101','111'], '1':['010','110','010','010','111'],
'2':['111','001','111','100','111'], '3':['111','001','111','001','111'],
'4':['101','101','111','001','001'], '5':['111','100','111','001','111'],
'6':['111','100','111','101','111'], '7':['111','001','010','010','010'],
'8':['111','101','111','101','111'], '9':['111','101','111','001','111'],
'S':['111','100','111','001','111'],
}
def raster_line(points, radius=.6):
"""Integer columns whose centres meet a piecewise line, with no diagonal holes."""
result=set()
for (ax,az),(bx,bz) in zip(points,points[1:]):
dx,dz=bx-ax,bz-az; length=dx*dx+dz*dz
for z in range(math.floor(min(az,bz)-radius),math.ceil(max(az,bz)+radius)+1):
for x in range(math.floor(min(ax,bx)-radius),math.ceil(max(ax,bx)+radius)+1):
t=max(0,min(1,((x-ax)*dx+(z-az)*dz)/length)) if length else 0
if (x-ax-t*dx)**2+(z-az-t*dz)**2 <= radius**2:
result.add((x,z))
return result
def offset(points, distance):
result=[]
for i,(x,z) in enumerate(points):
a=points[max(0,i-3)];b=points[min(len(points)-1,i+3)]
dx,dz=b[0]-a[0],b[1]-a[1];norm=math.hypot(dx,dz) or 1
result.append((x-dz/norm*distance,z+dx/norm*distance))
return result
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--study',type=Path,required=True);p.add_argument('--surface',type=Path,required=True)
p.add_argument('--scope',type=Path,required=True);p.add_argument('--output',type=Path,required=True)
args=p.parse_args();study=json.loads(args.study.read_text());surface=json.loads(args.surface.read_text())
scope=json.loads(args.scope.read_text())
if surface['world_uuid']!=scope['world_id'] or surface['world']!=study['world']:
raise ValueError('Study, world surface and authorization scope disagree')
W=surface['width'];MINX=surface['min_x'];MINZ=surface['min_z']
heights=surface['surface_y'];palette=surface['palette'];materials=surface['material_index']
desired={}; priorities={};skipped=Counter();raised=[]
def at(x,z):
if not MINX<=x<=surface['max_x'] or not MINZ<=z<=surface['max_z']:
return None,None
i=(z-MINZ)*W+x-MINX
return heights[i],palette[materials[i]]
def put(x,z,color,group,priority=10,y=None):
x,z=round(x),round(z);top,material=at(x,z)
if top is None:skipped['outside']+=1;return
if y is None:
if material=='minecraft:water':skipped['water']+=1;return
y=max(50,top)
if y<top:
# Fixed-height architectural reservations can cross rising land. Raise
# only the visible survey stroke; never excavate unknown subsurface.
raised.append({'x':x,'z':z,'requested_y':y,'surface_y':top,'group':group});y=top
if y<50:raise ValueError('Survey stroke would contact water')
expected=material if y==top else 'minecraft:air'
if expected=='minecraft:water':raise ValueError('Refusing to replace water')
if expected=='minecraft:grass_block':expected+='[snowy=false]'
block=('minecraft:'+color+'_concrete') if color in RGB else ('minecraft:'+color)
key=(x,y,z)
if priority>=priorities.get(key,-1):
desired[key]={'x':x,'y':y,'z':z,'block':block,'expected':expected,'group':group}
priorities[key]=priority
def stroke(points,color,group,radius=.6,priority=10,y=None):
for x,z in sorted(raster_line(points,radius)):
put(x,z,color,group,priority,y)
def text(value,cx,cz,color,group,scale=2):
width=(len(value)*4-1)*scale
for j,char in enumerate(value):
for row,bits in enumerate(FONT[char]):
for col,bit in enumerate(bits):
if bit=='1':
for dz in range(scale):
for dx in range(scale):
put(cx-width//2+j*4*scale+col*scale+dx,cz-5*scale//2+row*scale+dz,
color,group,35)
# The main route is a corridor reservation, with a clear green interior.
# Thin edge lines and spaced centre ticks keep the terrain visually dominant.
for route in study['routes']:
pts=route['points'];group=route['id'];width=route['width'];deck=route.get('deck_y')
is_bridge=route['role']=='bridge'
edgecolor='white' if width>=5 else 'light_gray'
if is_bridge:edgecolor='white'
for side in (-1,1):
stroke(offset(pts,side*(width-1)/2),edgecolor,group,.65,10,deck)
if width>=7:
for i in range(0,len(pts),18):
stroke(pts[i:i+3],'light_gray',group,.55,11,deck)
if is_bridge:
# Cross ties define the future deck without filling it or the river.
for i in range(0,len(pts),12):
a=offset(pts,-(width-1)/2)[i];b=offset(pts,(width-1)/2)[i]
stroke([a,b],'red' if group=='arrival-viaduct' else 'light_gray',group,.55,12,deck)
for end in (0,-1):
for side in (-1,1):
x,z=offset(pts,side*(width-1)/2)[end]
for y in range(deck+1,deck+5):put(x,z,'white',group,28,y)
put(x,z,'glowstone',group,29,deck+5)
for feature in study['features']:
color=COLORS[feature['district']];role=feature['role'];group=feature['id']
pts=feature['points'];deck=feature.get('deck_y')
if feature['type']=='polygon' and pts[0]!=pts[-1]:pts=pts+[pts[0]]
radius=1.05 if role in ('building','pier') or group=='arrival-hex' else .65
stroke(pts,color,group,radius,20,deck)
if role=='building':
# Four sparse survey stakes make footprints recognizable at eye level.
for point in [pts[i] for i in sorted(set([0,(len(pts)-1)//4,(len(pts)-1)//2,3*(len(pts)-1)//4]))]:
x,z=point;top,_=at(x,z)
if top is None:continue
for y in range(top+1,top+4):put(x,z,color,group,27,y)
put(x,z,'glowstone',group,28,top+4)
# Wayfinding IDs appear as blocks as well as external map labels.
# Small districts use one-block pixels to keep their reservations readable.
for district in study['districts']:
ident=district['id'];x,z=district['label'];color=COLORS[ident]
if ident=='09':continue
x,z={'01':(-22,27),'07':(26,271),'08':(-136,63)}.get(ident,(x,z))
text(ident,x,z,'white','label-'+ident,1 if ident=='08' else 2)
# The brand medallion remains a reserved ring; its S is a simple block glyph.
text('S',0,8,'lime','spawn-monogram',3)
# Monumental colored posts stand outside the main entry points, never in a path.
posts=[('01',33,48),('02',11,-73),('03',-151,-125),('04',197,-97),
('05',174,75),('06',-171,226),('07',18,287),('08',-132,43)]
labels=[]
for ident,x,z in posts:
top,material=at(x,z)
if material=='minecraft:water':raise ValueError('Wayfinding post in water')
color=COLORS[ident];group='wayfinding-'+ident
for xx in range(x-1,x+2):
for zz in range(z-1,z+2):put(xx,zz,color,group,40)
for y in range(top+1,top+9):put(x,z,color,group,40,y)
put(x,z,'glowstone',group,41,top+9)
for xx in (x-1,x+1):put(xx,z,color,group,40,top+7)
district=next(d for d in study['districts'] if d['id']==ident)
labels.append({'id':ident,'name':district['name'],'x':x+.5,'y':top+11,'z':z+.5,'color':RGB[color]})
blocks=sorted(desired.values(),key=lambda b:(b['z']//16,b['x']//16,b['y']//16,b['y'],b['z'],b['x']))
out={'version':1,'scope':scope,'blocks':blocks}
args.output.parent.mkdir(parents=True,exist_ok=True)
args.output.write_text(json.dumps(out,separators=(',',':'))+'\n')
metadata={'source_surface':str(args.surface),'world':surface['world'],'blocks':len(blocks),
'by_material':dict(Counter(b['block'] for b in blocks)),
'by_group':dict(Counter(b['group'] for b in blocks)),
'skipped_strokes':dict(skipped),'raised_fixed_height_strokes':raised,
'wayfinding_labels':labels,'districts':study['districts'],
'note':'Surface contours preserve height; bridge outlines reserve a future deck. Walkability requires later paths and stairs.'}
args.output.with_suffix('.metadata.json').write_text(json.dumps(metadata,indent=2)+'\n')
print(json.dumps({'blocks':len(blocks),'groups':len(metadata['by_group']),
'skipped':dict(skipped),'raised_fixed_height_strokes':len(raised)}))
if __name__=='__main__':main()
@@ -0,0 +1,114 @@
package probe;
import org.bukkit.*;
import org.bukkit.block.data.BlockData;
import org.bukkit.command.*;
import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.*;
import java.nio.file.*;
import java.time.Instant;
import java.util.*;
/** Test-only, console-only helper; never installed in the user's lobby server. */
public final class MaterialRegistryProbe extends JavaPlugin {
private final List<Map<String,Object>> failures = new ArrayList<>();
private int failureCount;
@Override public void onEnable() { getLogger().info("Use console registryprobe to run the isolated registry test"); }
@Override public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (sender instanceof ConsoleCommandSender) runProbe(); return true;
}
private static Method method(Class<?> c, String name, Class<?>... args) throws Exception {
Method result=c.getDeclaredMethod(name,args); result.setAccessible(true); return result;
}
private static Object call(Method m, Object receiver, Object... args) throws Throwable {
try { return m.invoke(receiver,args); } catch(InvocationTargetException e) { throw e.getCause(); }
}
private static void equal(Object expected,Object actual) { if (!Objects.equals(expected,actual)) throw new AssertionError("snapshot_mismatch"); }
private void failure(String id,String state,String phase,Throwable error) {
failureCount++;
if(failures.size()<128) failures.add(Map.of("block",id,"state",state,"phase",phase,"reason",error.getClass().getSimpleName()));
}
private void runProbe() {
long start=System.nanoTime(); failures.clear(); failureCount=0;
int total=0,passed=0,entityDefaults=0,catalogDescribed=0,registryCount=0,maxDescriptionBytes=0,maxProperties=0,maxPropertyValues=0;
int propertyCases=0,propertyPassed=0,freshPassed=0,sameMaterialPassed=0,privateUndoPassed=0;
World anchorWorld=null; BlockData anchorBefore=null; boolean anchorRestored=false;
String fatal=null; Map<?,?> catalogSummary=Map.of();
try {
var plugin=Bukkit.getPluginManager().getPlugin("MinecraftBuilderMCP"); var loader=plugin.getClass().getClassLoader();
Class<?> accessClass=Class.forName("io.github.minecraftbuilder.paper.BuildingWorld",true,loader);
Class<?> catalogClass=Class.forName("io.github.minecraftbuilder.paper.MaterialCatalog",true,loader);
Constructor<?> catalogConstructor=catalogClass.getDeclaredConstructor(); catalogConstructor.setAccessible(true);
Object catalog=catalogConstructor.newInstance(); Method describe=method(catalogClass,"describe",String.class);
catalogSummary=(Map<?,?>)call(method(catalogClass,"summary"),catalog); registryCount=(int)Registry.BLOCK.stream().count();
Class<?> posClass=Class.forName("io.github.minecraftbuilder.core.BlockPos",true,loader);
Constructor<?> constructor=accessClass.getDeclaredConstructor(World.class); constructor.setAccessible(true);
World world=Bukkit.getWorld("world");
if(world==null||Bukkit.getPort()!=25576) throw new IllegalStateException("IsolatedServerRequired");
world.loadChunk(0,0); Object pos=posClass.getConstructor(int.class,int.class,int.class).newInstance(4,100,4);
if(!world.getBlockAt(4,100,4).getType().isAir()||!world.getBlockAt(5,100,4).getType().isAir()) throw new IllegalStateException("AirFixtureRequired");
anchorWorld=world; anchorBefore=world.getBlockAt(5,100,4).getBlockData();
world.getBlockAt(5,100,4).setBlockData(Material.STONE.createBlockData(),false);
Object access=constructor.newInstance(world);
Method capture=method(accessClass,"captureBlock",posClass),prepare=method(accessClass,"prepareBlock",posClass,String.class,String.class),place=method(accessClass,"setCapturedBlock",posClass,String.class);
String before=(String)call(capture,access,pos);
for(Material material:Material.values()) {
if(material.isLegacy()||!material.isBlock()) continue;
total++; String id=material.getKey().toString(); Map<?,?> properties=Map.of();
try {
Map<?,?> description=(Map<?,?>)call(describe,catalog,id);
maxDescriptionBytes=Math.max(maxDescriptionBytes,new com.google.gson.Gson().toJson(description).getBytes(java.nio.charset.StandardCharsets.UTF_8).length);
properties=(Map<?,?>)description.get("properties"); maxProperties=Math.max(maxProperties,properties.size());
for(Object values:properties.values()) maxPropertyValues=Math.max(maxPropertyValues,((Collection<?>)values).size());
catalogDescribed++;
} catch(Throwable e) { failure(id,id,"catalog_description",e); }
String defaultState=material.createBlockData().getAsString(),phase="prepare_default",base=null;
try {
base=(String)call(prepare,access,pos,defaultState,before); if(base.startsWith("\u0000")) entityDefaults++;
equal(before,call(capture,access,pos));
phase="place_default"; call(place,access,pos,base);
phase="verify_default"; equal(base,call(capture,access,pos)); passed++;
} catch(Throwable e) { failure(id,defaultState,phase,e); }
finally { call(place,access,pos,before); equal(before,call(capture,access,pos)); }
if(base==null) continue;
Set<String> variants=new LinkedHashSet<>();
for(var property:properties.entrySet()) for(Object value:(Collection<?>)property.getValue())
variants.add(Bukkit.createBlockData(id+"["+property.getKey()+"="+value+"]").getAsString());
for(String variant:variants) {
if(++propertyCases>30_000||System.nanoTime()-start>35_000_000_000L) throw new IllegalStateException("ProbeBudgetExceeded");
phase="prepare_fresh_property";
try {
String desired=(String)call(prepare,access,pos,variant,before); equal(before,call(capture,access,pos));
phase="place_fresh_property"; call(place,access,pos,desired);
phase="verify_fresh_property"; equal(desired,call(capture,access,pos)); freshPassed++;
call(place,access,pos,before); equal(before,call(capture,access,pos));
phase="place_existing_default"; call(place,access,pos,base); equal(base,call(capture,access,pos));
phase="prepare_same_material"; String changed=(String)call(prepare,access,pos,variant,base); equal(base,call(capture,access,pos));
phase="place_same_material"; call(place,access,pos,changed);
phase="verify_same_material"; equal(changed,call(capture,access,pos)); sameMaterialPassed++;
phase="private_undo"; call(place,access,pos,base); equal(base,call(capture,access,pos)); privateUndoPassed++; propertyPassed++;
} catch(Throwable e) { failure(id,variant,phase,e); }
finally { call(place,access,pos,before); equal(before,call(capture,access,pos)); }
}
}
} catch(Throwable e) { fatal=e.getClass().getSimpleName(); }
finally {
if(anchorWorld!=null&&anchorBefore!=null) {
anchorWorld.getBlockAt(5,100,4).setBlockData(anchorBefore,false);
anchorRestored=anchorBefore.equals(anchorWorld.getBlockAt(5,100,4).getBlockData());
}
}
double millis=(System.nanoTime()-start)/1_000_000.0;
Map<String,Object> result=new LinkedHashMap<>();
result.put("timestamp",Instant.now().toString());result.put("server_version",Bukkit.getVersion());
result.put("fixture","Air target (4,100,4); temporary stone section anchor (5,100,4)");result.put("anchor_restored",anchorRestored);
result.put("catalog_summary",catalogSummary);result.put("catalog_described",catalogDescribed);result.put("registry_blocks",registryCount);
result.put("max_description_bytes",maxDescriptionBytes);result.put("max_properties",maxProperties);result.put("max_property_values",maxPropertyValues);
result.put("total",total);result.put("passed",passed);result.put("entity_defaults",entityDefaults);
result.put("property_cases",propertyCases);result.put("property_passed",propertyPassed);result.put("fresh_property_passed",freshPassed);
result.put("same_material_property_passed",sameMaterialPassed);result.put("private_undo_passed",privateUndoPassed);
result.put("duration_ms",millis);result.put("fatal",fatal);result.put("failure_count",failureCount);result.put("failures",failures);
try { getDataFolder().mkdirs(); Files.writeString(getDataFolder().toPath().resolve("report.json"),new com.google.gson.GsonBuilder().serializeNulls().create().toJson(result)); }
catch(Exception e) { getLogger().severe("Could not save registry probe report"); }
getLogger().info("Registry probe: "+passed+"/"+total+", property cases "+propertyPassed+"/"+propertyCases+", failures "+failureCount+", fatal "+fatal+", "+Math.round(millis)+" ms");
}
}
+25
View File
@@ -0,0 +1,25 @@
# Isolated material registry regression probe
This optional test plugin exercises the **installed production plugin** through reflection. It is excluded from Maven modules and never installed by the development server launcher or this builder.
It verifies every registered block default, every catalog description, and each distinct state obtained by varying one property at a time. Property cases cover fresh placement, changing an existing block of the same material, and exact private snapshot restoration. It also verifies that preparation does not modify the world. This is not a Cartesian enumeration of all property combinations or a test of later game ticks.
Build from the repository root after the development Paper runtime and JDK have been prepared:
```bash
python3 scripts/material-registry-probe/build.py
```
The only build outputs are under `.runtime/material-registry-probe/`. Use `--paper-home PATH` to select a prepared Paper installation for its dependency libraries and `--java-home PATH` to select another JDK 25 or newer.
Install **only into a disposable isolated test server**, using the pinned Paper version and the production plugin JAR being tested:
```bash
cp .runtime/material-registry-probe/material-registry-probe.jar .runtime/material-test-server/plugins/
```
Start or restart that isolated server, then enter `registryprobe` in its server console. The plugin does nothing on startup and ignores player invocations. It requires port **25576**, world **`world`**, and air at **(4, 100, 4)** and **(5, 100, 4)**. It temporarily uses the second position as a stone anchor, then restores both positions; the anchor makes `cave_air` and `void_air` observable because Minecraft treats entirely empty sections as ordinary air. Do not use a valuable world just because it happens to match these guards.
Read `plugins/MaterialRegistryProbe/report.json` in the isolated server directory. A successful report has matching default/property/catalog counts, `anchor_restored: true`, `fatal: null`, and zero failures. Reports contain only material/state identifiers, counts, timings and exception types—no block-entity payloads. The probe caps property cases at 30,000, execution at 35 seconds, and detailed failures at 128. The console command runs on the server thread; the test server should have no players. Remove the test plugin after use.
The initial Paper 26.2 verification covered 1,196 block defaults, 186 block-entity defaults, and 5,392 distinct property cases in approximately three seconds. These counts are observations, not hardcoded expectations; the probe follows the runtime registry.
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Build the opt-in isolated Paper regression probe; never install or run it."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import shutil
import subprocess
ROOT = Path(__file__).resolve().parents[2]
OUTPUT = ROOT / ".runtime" / "material-registry-probe"
PLUGIN = """name: MaterialRegistryProbe
version: '1.0'
main: probe.MaterialRegistryProbe
api-version: '26.2'
depend: [MinecraftBuilderMCP]
commands:
registryprobe:
description: Run isolated all-registry block snapshot verification
"""
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--paper-home", type=Path, default=ROOT / ".runtime" / "server",
help="Prepared Paper installation whose libraries supply the compile classpath")
parser.add_argument("--java-home", type=Path,
default=Path(os.environ.get("MCB_JAVA_HOME", str(Path.home() / ".cache" / "minecraft-builder-mcp" / "jdk-25.0.2"))),
help="JDK 25 or newer; defaults to the project's downloaded JDK")
args = parser.parse_args()
libraries = sorted((args.paper_home / "libraries").rglob("*.jar"))
if not libraries:
parser.error("No Paper dependency jars found; prepare the local development server first")
javac, jar = args.java_home / "bin" / "javac", args.java_home / "bin" / "jar"
if not javac.is_file() or not jar.is_file():
parser.error("JDK javac/jar unavailable; supply --java-home or MCB_JAVA_HOME")
classes = OUTPUT / "classes"
if classes.exists():
shutil.rmtree(classes)
classes.mkdir(parents=True)
subprocess.run([str(javac), "--release", "25", "-cp", os.pathsep.join(str(p.resolve()) for p in libraries),
"-d", str(classes), str(Path(__file__).with_name("MaterialRegistryProbe.java"))], check=True)
(classes / "plugin.yml").write_text(PLUGIN, encoding="utf-8")
output = OUTPUT / "material-registry-probe.jar"
subprocess.run([str(jar), "--create", "--file", str(output), "-C", str(classes), "."], check=True)
print(output)
if __name__ == "__main__":
main()
+177
View File
@@ -0,0 +1,177 @@
"""Small, stateless voxel assets for the Shacraft arrival gardens.
Every public builder returns ``{(x, y, z): full_block_state}`` in local block
coordinates. Origin Y is the first air block above the paving/soil: translate
by Y96 for a paving block at Y95. No builder reads or changes a Minecraft world.
Trees need soil under their trunk; the composer owns foundations, collision
checks, leaf-distance propagation, block connection updates, and placement.
"""
from __future__ import annotations
import json
import math
import random
Position = tuple[int, int, int]
Asset = dict[Position, str]
_DIRECTIONS = ("north", "east", "south", "west")
def _state(material: str, **properties: object) -> str:
suffix = ",".join(
f"{key}={str(value).lower()}" for key, value in sorted(properties.items())
)
return f"minecraft:{material}" + (f"[{suffix}]" if suffix else "")
def _stair(material: str, facing: str) -> str:
return _state(material, facing=facing, half="bottom", shape="straight", waterlogged=False)
def _connections(material: str, *directions: str) -> str:
return _state(material, **{d: d in directions for d in _DIRECTIONS}, waterlogged=False)
def _rotate(asset: Asset, quarter_turns: int) -> Asset:
"""Rotate north to east, including stair backs and fence/bar connections."""
result: Asset = {}
direction = {d: _DIRECTIONS[(i + quarter_turns) % 4] for i, d in enumerate(_DIRECTIONS)}
for (x, y, z), state in asset.items():
for _ in range(quarter_turns):
x, z = -z, x
if "[" in state:
material, raw = state[:-1].split("[", 1)
properties = dict(pair.split("=", 1) for pair in raw.split(","))
properties = {direction.get(k, k): direction.get(v, v) for k, v in properties.items()}
if quarter_turns % 2 and properties.get("axis") in ("x", "z"):
properties["axis"] = "z" if properties["axis"] == "x" else "x"
state = _state(material.removeprefix("minecraft:"), **properties)
result[x, y, z] = state
return result
def conifer(height: int = 13, seed: int = 0) -> Asset:
"""A narrow spruce with irregular connected whorls and three clear trunk rows.
Height is the occupied block count, 11..15. The maximum canopy radius is
three blocks, and foliage begins at local Y3. Leaves deliberately start at
distance=7; recompute distances against the composed world before applying.
"""
if type(height) is not int or not 11 <= height <= 15:
raise ValueError("Conifer height must be an integer from 11 through 15")
if type(seed) is not int:
raise ValueError("Conifer seed must be an integer")
rng = random.Random(seed)
phase = rng.uniform(0, math.tau)
phase2 = rng.uniform(0, math.tau)
leaves = _state("spruce_leaves", distance=7, persistent=True, waterlogged=False)
asset: Asset = {}
for y in range(3, height):
progress = (y - 3) / (height - 4)
tier = (0.30, -0.30, -0.65)[(y - 3) % 3]
radius = max(0.0, 2.95 * (1.0 - progress) ** 0.85 + tier)
if y == height - 1:
radius = 0.0
for x in range(-3, 4):
for z in range(-3, 4):
angle = math.atan2(z, x)
edge = radius + 0.23 * math.cos(4 * angle + phase) + 0.16 * math.sin(3 * angle + phase2)
edge += rng.uniform(-0.10, 0.10)
if (x == 0 and z == 0) or math.hypot(x, z) <= edge:
asset[x, y, z] = leaves
# Leave a connected green leader above the last woody branch; overwriting
# these narrow upper layers with logs creates visible brown pegs at the tip.
for y in range(height - 5):
asset[0, y, 0] = _state("spruce_log", axis="y")
# Angular variation can leave a diagonal-only leaf at a narrow upper tier.
# Keep the face-connected canopy so no isolated foliage floats beside it.
connected = {(0, 0, 0)}
pending = [(0, 0, 0)]
while pending:
x, y, z = pending.pop()
for dx, dy, dz in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)):
neighbor = (x + dx, y + dy, z + dz)
if neighbor in asset and neighbor not in connected:
connected.add(neighbor)
pending.append(neighbor)
return {position: state for position, state in asset.items() if position in connected}
def bench(length: int = 5, facing: str = "north") -> Asset:
"""A 3..5 block overall bench, including its two stone armrests.
``facing`` is the seated viewer's direction, not Minecraft's stair-facing
property: a north-looking seat has a south-facing stair/high back. The
default occupies X=-2..2, Z=0..1, Y=0; its front opens toward negative Z.
The fence back has Minecraft's 1.5-block collision height. This is decorative
seating and does not add a sit interaction.
"""
if type(length) is not int or not 3 <= length <= 5:
raise ValueError("Bench overall length must be an integer from 3 through 5")
if facing not in _DIRECTIONS:
raise ValueError("Bench facing must be north, east, south, or west")
first = -(length // 2)
last = first + length - 1
asset: Asset = {}
for x in range(first + 1, last):
asset[x, 0, 0] = _stair("spruce_stairs", "south")
asset[x, 0, 1] = _connections("spruce_fence", "north", "east", "west")
for x in (first, last):
asset[x, 0, 0] = _state("stone_bricks")
asset[x, 0, 1] = _state("stone_bricks")
return _rotate(asset, _DIRECTIONS.index(facing))
def lamp(height: int = 6) -> Asset:
"""A slender warm street lamp with a copper hood and a small brass collar.
Height is 6..8 occupied blocks. The stone/chain stem occupies one column;
the 3x3 cross-shaped housing begins at height-3, above pedestrian headroom.
Its hanging lantern has a full copper block immediately above it. The four
inward-facing copper stairs form the hood eaves; no trapdoors are used.
"""
if type(height) is not int or not 6 <= height <= 8:
raise ValueError("Lamp height must be an integer from 6 through 8")
collar_y = height - 3
light_y = height - 2
roof_y = height - 1
asset: Asset = {
(0, 0, 0): _state("chiseled_stone_bricks"),
(0, 1, 0): _state("stone_brick_wall", east="none", north="none", south="none", up=True, waterlogged=False, west="none"),
(0, collar_y, 0): _state("gold_block"),
(0, light_y, 0): _state("lantern", hanging=True, waterlogged=False),
(0, roof_y, 0): _state("waxed_oxidized_cut_copper"),
}
for y in range(2, collar_y):
asset[0, y, 0] = _state("iron_chain", axis="y", waterlogged=False)
for x, z, inward in ((-1, 0, "east"), (1, 0, "west"), (0, -1, "south"), (0, 1, "north")):
asset[x, collar_y, z] = _connections("iron_bars", inward)
asset[x, light_y, z] = _connections("iron_bars")
asset[x, roof_y, z] = _stair("waxed_oxidized_cut_copper_stairs", inward)
return asset
def describe(asset: Asset) -> dict:
"""Compact occupied bounds and ground contact cells for the layout composer."""
if not asset:
raise ValueError("Cannot describe an empty asset")
low = [min(p[i] for p in asset) for i in range(3)]
high = [max(p[i] for p in asset) for i in range(3)]
return {
"blocks": len(asset),
"min": dict(zip(("x", "y", "z"), low)),
"max": dict(zip(("x", "y", "z"), high)),
"size": dict(zip(("x", "y", "z"), (high[i] - low[i] + 1 for i in range(3)))),
"ground_contacts": sorted([x, z] for x, y, z in asset if y == 0),
"materials": sorted({state.split("[", 1)[0] for state in asset.values()}),
}
if __name__ == "__main__":
print(json.dumps({
"coordinate_convention": "Local Y0 is first air above paving; add Y96 over paving Y95.",
"conifer": describe(conifer()),
"bench_north": describe(bench()),
"lamp": describe(lamp()),
}, indent=2))
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Render a survey atlas from live surface data; optional blocks are a labelled preview."""
import argparse
import json
import math
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.colors import to_rgb, LightSource
ROOT=Path(__file__).resolve().parents[1]
COLOR={'01':'#98d84d','02':'#f6d34a','03':'#9460ce','04':'#23b6b6','05':'#e3544b',
'06':'#f78d27','07':'#f0f0e6','08':'#4a69d8','09':'#f394b5'}
BLOCK={'lime':'#98d84d','yellow':'#f6d34a','purple':'#9460ce','cyan':'#23b6b6',
'green':'#527c31','orange':'#f78d27','white':'#f0f0e6','blue':'#4a69d8',
'pink':'#f394b5','black':'#26282d','light_gray':'#a4aaa6','red':'#e3544b'}
def main():
p=argparse.ArgumentParser();p.add_argument('surface',type=Path);p.add_argument('--study',type=Path,required=True)
p.add_argument('--blocks',type=Path);p.add_argument('--output',type=Path,required=True)
args=p.parse_args();s=json.loads(args.surface.read_text());study=json.loads(args.study.read_text())
h=np.array(s['surface_y']).reshape(s['length'],s['width']).astype(float)
indexes=np.array(s['material_index']).reshape(h.shape);palette=list(s['palette']);colors=list(s['palette_rgb'])
if args.blocks:
for b in json.loads(args.blocks.read_text())['blocks']:
x,z=b['x']-s['min_x'],b['z']-s['min_z']
if b['y']<h[z,x]:continue
material=b['block']
if material not in palette:
palette.append(material)
colors.append(BLOCK.get(material.replace('minecraft:','').replace('_concrete',''),'#f4cf58'))
indexes[z,x]=palette.index(material);h[z,x]=b['y']
rgb=np.array([to_rgb(c) for c in colors])[indexes]
dz,dx=np.gradient(h);norm=np.sqrt(dx*dx+dz*dz+1)
shade=np.clip(.76+.36*(.55*dx+.55*dz+.63)/norm,.44,1.13)
categorical=np.array([m.endswith('_concrete') or m=='minecraft:glowstone' for m in palette])[indexes]
water=np.array([m=='minecraft:water' for m in palette])[indexes]
shade=np.where(categorical,1,np.where(water,.96,shade))
rgb=np.clip(rgb*shade[...,None],0,1)
bg='#14211e';fg='#e8eade';muted='#a9bcb0'
fig=plt.figure(figsize=(16,12),facecolor=bg)
ax=fig.add_axes([.045,.12,.67,.80],facecolor=bg)
ax.imshow(rgb,extent=(s['min_x']-.5,s['max_x']+.5,s['max_z']+.5,s['min_z']-.5),interpolation='nearest')
ax.set_xlim(-320,319);ax.set_ylim(384,-255)
ax.set_xticks(np.arange(-256,320,64));ax.set_yticks(np.arange(-192,384,64))
ax.tick_params(colors=muted,labelsize=9);ax.set_xlabel('X / blocks',color=muted,labelpad=8)
ax.set_ylabel('Z / blocks · north up',color=muted,labelpad=8)
for spine in ax.spines.values():spine.set_color('#607166')
for d in study['districts']:
x,z=d['label'];ident=d['id']
ax.text(x,z-13,ident,ha='center',va='center',fontsize=12,fontweight='bold',color='#14211e',
bbox=dict(boxstyle='circle,pad=.30',facecolor=COLOR[ident],edgecolor=fg,linewidth=1.1),zorder=9)
ax.annotate('N',xy=(287,-224),xytext=(287,-198),ha='center',color=fg,fontsize=12,
arrowprops=dict(arrowstyle='-|>',color=fg,lw=1.5))
ax.plot([-288,-224],[355,355],color=fg,lw=3);ax.text(-256,370,'64 blocks',color=fg,ha='center',fontsize=9)
fig.text(.06,.953,'SHACRAFT',color=fg,fontsize=28,fontweight='bold')
fig.text(.242,.958,'LOBBY / SITE MARKING',color=muted,fontsize=16)
fig.text(.746,.881,'DISTRICTS',color=muted,fontsize=12,fontweight='bold')
descriptions={
'01':'Hexagonal arrival plaza\nCentral Shacraft medallion',
'02':'Clock tower + station hall\nPavilions and forecourt',
'03':'Portal concourse\nSix individual portal bays',
'04':'Airship terminal\nThree piers + flagship reserve',
'05':'Palm house · winter garden\nObservatory and garden walks',
'06':'Market square\nSix separate building plots',
'07':'Arrival avenue and viaduct\nSouthern entrance to the valley',
'08':'Lake pumping house\nBoardwalk and viewing terrace',
'09':'Scenic overlooks\nSmall optional ridge trails'}
for i,d in enumerate(study['districts']):
y=.842-i*.067;ident=d['id']
fig.text(.749,y,ident,color=COLOR[ident],fontsize=16,fontweight='bold')
fig.text(.779,y,descriptions[ident],color=fg,fontsize=10,linespacing=1.55)
fig.text(.747,.20,'READING THE MARKS',color=muted,fontsize=11,fontweight='bold')
fig.text(.747,.169,'Color = building / courtyard boundary\nWhite = future path edges\nRaised ribs = bridge deck reservation\nLit stakes = corners and wayfinding',color=fg,fontsize=9,linespacing=1.7,va='top')
note='DESIGN PREVIEW · proposed blocks over a live survey' if args.blocks else 'ACTUAL WORLD SURFACE · captured after placement · no player camera required'
fig.text(.06,.055,note,color=fg,fontsize=10)
fig.text(.06,.035,'768 × 768 world · central development shown · terrain heights and water preserved · paths and stairs are still reservations',color=muted,fontsize=9)
args.output.parent.mkdir(parents=True,exist_ok=True);fig.savefig(args.output,dpi=150,facecolor=bg);plt.close(fig)
print(args.output)
if __name__=='__main__':main()
+423
View File
@@ -0,0 +1,423 @@
#!/usr/bin/env python3
"""Deterministic, detached exterior for the approved two-floor Shacraft station.
The caller supplies the exact station foundation cells and combines this shell with
the interior before taking expected-state snapshots. This module never reads or
writes a Minecraft world. Coordinates and block states are explicit and stable.
"""
from collections import Counter, defaultdict
from math import ceil, hypot
AIR = "minecraft:air"
STONE = "minecraft:stone_bricks"
DARK = "minecraft:polished_deepslate"
CREAM = "minecraft:smooth_sandstone"
ASHLAR = "minecraft:cut_sandstone"
PALE = "minecraft:smooth_quartz"
PILLAR = "minecraft:quartz_pillar[axis=y]"
GREEN = "minecraft:waxed_oxidized_cut_copper"
DEEP_GREEN = "minecraft:green_concrete"
GOLD = "minecraft:gold_block"
WOOD = "minecraft:spruce_planks"
GLASS = "minecraft:brown_stained_glass"
CLEAR_GLASS = "minecraft:glass"
LAMP = "minecraft:lantern[hanging=true,waterlogged=false]"
CHAIN = "minecraft:iron_chain[axis=y,waterlogged=false]"
GLOW = "minecraft:glowstone"
CARDINALS = ((0, -1, "north"), (1, 0, "east"), (0, 1, "south"), (-1, 0, "west"))
def _dilate(cells, radius):
return {(x + dx, z + dz) for x, z in cells
for dx in range(-radius, radius + 1) for dz in range(-radius, radius + 1)}
def _erode(cells, radius):
return {(x, z) for x, z in cells if all((x + dx, z + dz) in cells
for dx in range(-radius, radius + 1) for dz in range(-radius, radius + 1))}
def _runs(values):
result = []
for value in sorted(values):
if result and value == result[-1][-1] + 1:
result[-1].append(value)
else:
result.append([value])
return result
def _line(x0, y0, x1, y1):
"""Inclusive integer line for legible clock hands and roof trim."""
dx, dy = abs(x1 - x0), -abs(y1 - y0)
sx, sy = 1 if x0 < x1 else -1, 1 if y0 < y1 else -1
error = dx + dy
while True:
yield x0, y0
if (x0, y0) == (x1, y1):
return
twice = 2 * error
if twice >= dy:
error += dy
x0 += sx
if twice <= dx:
error += dx
y0 += sy
def compile_exterior(footprint: set[tuple[int, int]], layout: dict):
"""Return (block states, ownership groups, metadata), with no external effects.
Structural floors are exactly the supplied footprint. Interior ornament may
replace their finish, but must retain the two complete separating decks.
All exterior windows are full glass blocks, including their inner wall layer.
Only the south entrance removes wall blocks below the sealed roof space.
"""
footprint = {(int(x), int(z)) for x, z in footprint}
if not footprint:
raise ValueError("Station foundation must not be empty")
if any(not (-74 <= x <= 40 and -145 <= z <= -83) for x, z in footprint):
raise ValueError("Foundation exceeds the approved station envelope")
if layout.get("perimeter_wall_thickness", 2) != 2:
raise ValueError("The approved shell requires two-block perimeter walls")
inner = _erode(footprint, 2)
walls = footprint - inner
boundary = footprint - _erode(footprint, 1)
shadow = _dilate(footprint, 2)
maximum_shadow = _dilate(footprint, 3)
states, groups = {}, {}
def put(x, y, z, state, group):
x, y, z = int(x), int(y), int(z)
if not (-78 <= x <= 44 and -149 <= z <= -80 and 98 <= y <= 160):
raise ValueError(f"Exterior escaped approved coordinate bounds: {(x, y, z)}")
if (x, z) not in maximum_shadow:
raise ValueError(f"Exterior overhang exceeds three blocks: {(x, y, z)}")
if y == 98 and (x, z) not in footprint:
raise ValueError(f"Ground floor escaped foundation: {(x, y, z)}")
states[x, y, z] = state
groups[x, y, z] = group
def box(x0, x1, y0, y1, z0, z1, state, group, mask=None):
for z in range(z0, z1 + 1):
for x in range(x0, x1 + 1):
if mask is not None and (x, z) not in mask:
continue
for y in range(y0, y1 + 1):
put(x, y, z, state, group)
def stair(material, facing, half="bottom"):
return f"minecraft:{material}[facing={facing},half={half},shape=straight,waterlogged=false]"
# The upper cabin will be stationary; neither deck contains a lift-shaft hole.
for x, z in sorted(footprint):
for y, state, group in ((98, CREAM, "floor.ground"),
(111, WOOD, "floor.intermediate.structure"),
(112, CREAM, "floor.upper"),
(124, WOOD, "ceiling.upper.structure"),
(125, CREAM, "ceiling.upper.seal")):
put(x, y, z, state, group)
for x, z in sorted(walls):
for y in range(99, 125):
state = ASHLAR
if y == 99:
state = DARK
elif y == 100:
state = STONE
elif y in (101, 109, 113, 122, 123):
state = CREAM
elif y in (110, 111):
state = STONE
elif y in (112, 124):
state = PALE
put(x, y, z, state, "wall.masonry")
# Identify straight stretches from the exact contour, including its eastern recess.
faces = []
for nx, nz, facing in CARDINALS:
planes = defaultdict(list)
for x, z in boundary:
if (x + nx, z + nz) not in footprint:
planes[z if nz else x].append(x if nz else z)
for plane, positions in sorted(planes.items()):
for run in _runs(positions):
if len(run) >= 7:
faces.append((nx, nz, facing, plane, run[0], run[-1]))
def face_position(nx, nz, plane, tangent, depth=0):
# Positive depth is toward the inside; negative depth is a relief projection.
return (tangent - nx * depth, plane - nz * depth) if nz else (plane - nx * depth, tangent - nz * depth)
windows, pilasters, facade_lamps = [], [], []
for nx, nz, facing, plane, lo, hi in faces:
phase = -6 if nz else -119
centers = [c for c in range(lo + 3, hi - 2) if (c - phase) % 10 == 0]
if not centers and hi - lo >= 8:
centers = [(lo + hi) // 2]
for center in centers:
for base, top in ((102, 108), (115, 122)):
# A five-wide pointed arch, enclosed by cream voussoirs and piers.
for tangent in range(center - 3, center + 4):
delta = abs(tangent - center)
for y in range(base - 1, top + 2):
cap = top - max(0, delta - 1)
opening = delta <= 2 and base <= y <= cap
state = GLASS if opening else CREAM
if opening and tangent == center and y < top - 1:
state = WOOD
if opening and y == base + 3:
state = WOOD
if delta == 3 and y <= top - 1:
state = PILLAR
for depth in (0, 1):
x, z = face_position(nx, nz, plane, tangent, depth)
if (x, z) in walls:
put(x, y, z, state, "window.frame" if state != GLASS else "window.glass")
# Bottom sill projects by one block but never opens the shell.
for tangent in range(center - 3, center + 4):
x, z = face_position(nx, nz, plane, tangent, -1)
if (x, z) in maximum_shadow:
put(x, base - 1, z, CREAM, "window.sill")
windows.append({"normal": facing, "plane": plane, "center": center,
"base_y": base, "apex_y": top, "glass_layers": 2})
# Vertical bays use quiet, regular dressed-stone pilasters, not material noise.
pier_centers = sorted({lo, hi, *[c + 5 for c in centers if c + 5 <= hi]})
for center in pier_centers:
x, z = face_position(nx, nz, plane, center)
for depth in (0, 1):
px, pz = face_position(nx, nz, plane, center, depth)
if (px, pz) not in walls:
continue
for y in range(101, 124):
put(px, y, pz, PILLAR if y not in (110, 111, 112) else PALE, "wall.pilaster")
pilasters.append([x, z])
if center not in (lo, hi) and hi - lo >= 15:
px, pz = face_position(nx, nz, plane, center, -1)
# Eave-mounted lighting: clear below, and supported immediately above.
put(px, 124, pz, CREAM, "lamp.bracket")
put(px, 123, pz, CHAIN, "lamp.chain")
put(px, 122, pz, LAMP, "lamp.lantern")
facade_lamps.append([px, 122, pz])
# Continuous layered cornices connect every projection of the exact footprint.
first_relief = _dilate(footprint, 1) - inner
second_relief = shadow - _erode(footprint, 1)
for x, z in sorted(first_relief):
put(x, 110, z, CREAM, "cornice.floor.lower")
put(x, 112, z, PALE, "cornice.floor.upper")
put(x, 124, z, CREAM, "cornice.eave.lower")
for x, z in sorted(second_relief):
put(x, 125, z, CREAM, "cornice.eave.upper")
# Union of three pitched masses: a broad central hall and two hipped end wings.
# Every roof column is closed, without making the inaccessible attic solid fill.
def roof_height(x, z):
candidates = []
if -53 <= x <= 18 and -147 <= z <= -95:
candidates.append(140 - ceil(abs(z + 121) * 14 / 26))
if -76 <= x <= -50 and -141 <= z <= -96:
candidates.append(126 + max(0, min(13 - abs(x + 63), z + 141, -96 - z)))
if 14 <= x <= 42 and -141 <= z <= -96:
candidates.append(126 + max(0, min(14 - abs(x - 28), z + 141, -96 - z)))
if -27 <= x <= 15 and -100 <= z <= -81:
candidates.append(126 + max(0, min(10 - abs(z + 91), x + 27, 15 - x)))
return max([126, *candidates])
roof_heights = {p: min(140, roof_height(*p)) for p in shadow}
shadow_boundary = shadow - _erode(shadow, 1)
for x, z in sorted(shadow):
top = roof_heights[x, z]
# Close all verge/gable faces down to the continuous eave line.
if (x, z) in shadow_boundary:
for y in range(126, top):
put(x, y, z, ASHLAR if y < top - 1 else GREEN, "roof.gable")
put(x, top - 1, z, GREEN, "roof.underlay")
uphill = [(roof_heights.get((x + dx, z + dz), top - 1), facing)
for dx, dz, facing in CARDINALS]
high, facing = max(uphill, key=lambda item: item[0])
state = stair("waxed_oxidized_cut_copper_stairs", facing) if high > top else GREEN
if (x + 6) % 12 == 0 and top < 139:
state = GREEN
put(x, top, z, state, "roof.copper")
if top == 140:
put(x, 140, z, GREEN, "roof.ridge")
# Four pavilion lantern roofs lend a clear rhythm to the ends of the facade.
pavilions = [(-66, -131, 7), (-66, -106, 7), (32, -131, 7), (28, -106, 6)]
for cx, cz, radius in pavilions:
pavilion = {(x, z) for x in range(cx - radius, cx + radius + 1)
for z in range(cz - radius, cz + radius + 1) if (x, z) in shadow}
for x, z in sorted(pavilion):
ring = max(abs(x - cx), abs(z - cz))
top = 137 - ring
# The cap only raises the parent roof; it never cuts an accidental opening.
if top <= roof_heights[x, z]:
continue
for y in range(roof_heights[x, z], top):
put(x, y, z, GREEN if y >= top - 1 else CREAM, "pavilion.roof.support")
facing = "east" if x < cx else "west" if x > cx else "south" if z < cz else "north"
put(x, top, z, stair("waxed_oxidized_cut_copper_stairs", facing) if ring else GREEN, "pavilion.roof.copper")
for y, state in ((138, GREEN), (139, GOLD), (140, CHAIN)):
put(cx, y, cz, state, "pavilion.finial")
# Shacraft-green hanging stone panels, with a small gold S motif on the end bays.
# These are block reliefs, not entities or inventory-backed banner blocks.
shield_centers = [(-65, -97, 1), (28, -97, 1), (-65, -140, -1), (31, -140, -1)]
glyph = ("111", "100", "111", "001", "111")
for cx, plane, normal in shield_centers:
for y in range(110, 123):
half = 2 if y >= 112 else y - 109
for dx in range(-half, half + 1):
put(cx + dx, y, plane, GOLD if abs(dx) == half else DEEP_GREEN, "ornament.green.shield")
for row, bits in enumerate(glyph):
for col, bit in enumerate(bits):
if bit == "1":
put(cx + col - 1, 120 - row, plane + normal, GOLD, "ornament.gold.s")
for dx in range(-3, 4):
put(cx + dx, 123, plane, CREAM, "ornament.shield.cap")
# The projecting entrance arch remains seven blocks clear at useful head height.
axis = int(layout.get("entrance_axis_x", -6))
entrance = layout.get("entrance_opening_x", [-9, -3])
for x in range(axis - 6, axis + 7):
dx = abs(x - axis)
for z in (-85, -84):
if (x, z) not in footprint:
continue
for y in range(99, 111):
if dx in (4, 5):
put(x, y, z, PILLAR if y > 100 else DARK, "entrance.pier")
elif y >= 109 - min(dx, 4):
put(x, y, z, CREAM, "entrance.arch")
entrance_air = []
for x in range(int(entrance[0]), int(entrance[1]) + 1):
top = 109 - abs(x - axis)
for z in (-85, -84, -83):
for y in range(99, top + 1):
put(x, y, z, AIR, "entrance.clear")
entrance_air.append([x, y, z])
for px in (axis - 6, axis + 6):
put(px, 108, -83, CREAM, "entrance.lamp.bracket")
put(px, 108, -82, CREAM, "entrance.lamp.bracket")
put(px, 107, -82, CHAIN, "entrance.lamp.chain")
put(px, 106, -82, LAMP, "entrance.lamp")
# A high, glazed lancet over the portal echoes the long window below the reference clock.
for x in range(axis - 3, axis + 4):
for y in range(115, 124):
cap = 123 - abs(x - axis)
state = GLASS if y <= cap else CREAM
if x == axis or y == 118:
state = WOOD if y <= cap else CREAM
for z in (-85, -84):
if (x, z) in walls:
put(x, y, z, state, "entrance.upper.lancet")
# Sealed clock tower: solid underside already exists at Y124/125, no future-floor doorway.
tx0, tx1, tz0, tz1 = axis - 9, axis + 9, -102, -84
# Carry the tower's front corner piers down through both public-storey facades.
for x in (tx0, tx0 + 1, tx1 - 1, tx1):
for z in (tz1, tz1 - 1):
for y in range(101, 125):
put(x, y, z, PALE if y in (110, 111, 112, 124) else PILLAR, "tower.facade.pier")
tower = {(x, z) for x in range(tx0, tx1 + 1) for z in range(tz0, tz1 + 1)}
tower_inner = _erode(tower, 2)
tower_wall = tower - tower_inner
for x, z in sorted(tower):
put(x, 126, z, CREAM, "tower.base.seal")
for x, z in sorted(tower_wall):
corner = (x <= tx0 + 1 or x >= tx1 - 1) and (z <= tz0 + 1 or z >= tz1 - 1)
for y in range(127, 148):
state = PILLAR if corner else ASHLAR
if y in (128, 130, 147):
state = PALE
put(x, y, z, state, "tower.masonry")
# Clock disks have a stone backing, recessed cream face and four gold bezels.
clock_faces = [("south", axis, -82), ("north", axis, -104),
("west", -16, -93), ("east", 4, -93)]
hour_marks = {(0, 6), (0, -6), (6, 0), (-6, 0), (4, 4), (-4, 4), (4, -4), (-4, -4)}
hands = set(_line(0, 0, -3, 4)) | set(_line(0, 0, 3, 3))
for facing, center, plane in clock_faces:
nx, nz = {"south": (0, 1), "north": (0, -1), "west": (-1, 0), "east": (1, 0)}[facing]
for u in range(-8, 9):
for v in range(-8, 9):
radius = hypot(u, v)
if radius > 8.3:
continue
x, z = (center + u, plane) if nz else (center, plane + u)
# All ornament is supported by one continuous backing layer.
put(x - nx, 139 + v, z - nz, CREAM, "clock.backing")
state = CREAM
if radius > 7.4:
state = PALE
elif radius > 6.5:
state = GOLD
elif (u, v) in hour_marks:
state = DEEP_GREEN
elif (u, v) in hands:
state = DARK
if (u, v) == (0, 0):
state = GOLD
put(x, 139 + v, z, state, "clock.face." + facing)
# Broad cream cap, steep patinated copper crown and a restrained gold finial.
tower_cap = _dilate(tower, 1)
for x, z in sorted(tower_cap):
put(x, 148, z, PALE, "tower.cornice")
for x, z in sorted(tower):
radius = max(abs(x - axis), abs(z + 93))
top = 149 + max(0, 7 - radius)
for y in range(149, top):
put(x, y, z, GREEN, "tower.roof.underlay")
facing = "east" if x < axis else "west" if x > axis else "south" if z < -93 else "north"
put(x, top, z, stair("waxed_oxidized_cut_copper_stairs", facing) if radius else GREEN, "tower.roof.copper")
for x, z in ((tx0, tz0), (tx1, tz0), (tx0, tz1), (tx1, tz1)):
put(x, 150, z, GREEN, "tower.corner.finial")
put(x, 151, z, GOLD, "tower.corner.finial")
put(x, 152, z, CHAIN, "tower.corner.finial")
for y, state in ((157, GOLD), (158, CHAIN)):
put(axis, y, -93, state, "tower.central.finial")
for x, z in ((tx0 - 1, tz1), (tx1 + 1, tz1), (tx0 - 1, tz0), (tx1 + 1, tz0)):
put(x, 148, z, PALE, "tower.lamp.bracket")
put(x, 147, z, CHAIN, "tower.lamp.chain")
put(x, 146, z, LAMP, "tower.lamp")
# Deterministic checks describe the shell; the root performs full merged navigation QA.
entrance_columns = {(x, z) for x in range(entrance[0], entrance[1] + 1) for z in (-85, -84, -83)}
assert all(states.get((x, y, z)) == AIR for x, z in entrance_columns for y in range(99, 103))
assert all((x, z) in footprint for (x, y, z) in states if y == 98)
assert all(states[x, 112, z] != AIR and states[x, 125, z] != AIR for x, z in footprint)
assert {p for p, state in states.items() if state == AIR} == {tuple(p) for p in entrance_air}
solid = {p for p, state in states.items() if state != AIR}
remaining = set(solid)
queue = [remaining.pop()]
while queue:
x, y, z = queue.pop()
for p in ((x + 1, y, z), (x - 1, y, z), (x, y + 1, z),
(x, y - 1, z), (x, y, z + 1), (x, y, z - 1)):
if p in remaining:
remaining.remove(p)
queue.append(p)
assert not remaining, "Exterior contains detached floating ornament"
metadata = {
"generator": "station-exterior-v1", "world_writes": 0,
"foundation_columns": len(footprint), "two_block_wall_columns": len(walls),
"bounds": {"min": [min(p[i] for p in states) for i in range(3)],
"max": [max(p[i] for p in states) for i in range(3)]},
"floor_block_y": [98, 112], "walk_y": [99, 113],
"solid_intermediate_deck": [111, 112], "sealed_roof_ceiling": [124, 125],
"main_roof_eaves_y": 126, "main_roof_ridge_y": 140, "clocktower_peak_y": 158,
"entrance_clear_x": list(entrance), "entrance_clear_z": [-85, -84, -83],
"entrance_minimum_clear_height": 8, "windows": windows, "pilasters": pilasters,
"facade_lamps": facade_lamps, "pavilions": [list(p) for p in pavilions],
"clock_faces": [{"facing": f, "center_or_x": c, "plane_or_z": p,
"center_y": 139, "radius": 8} for f, c, p in clock_faces],
"materials": sorted({state.split("[")[0] for state in states.values()}),
"states_by_group": dict(sorted(Counter(groups.values()).items())),
"blocks": len(states), "entrance_air_blocks": len(entrance_air),
"checks": {"ground_floor_on_exact_footprint": True, "two_solid_separating_decks": True,
"only_main_entrance_is_open": True, "roof_and_tower_have_no_doorway": True,
"maximum_overhang_blocks": 3, "all_windows_full_block_glass": True,
"all_solid_states_share_one_cardinally_connected_component": True},
}
return states, groups, metadata
+514
View File
@@ -0,0 +1,514 @@
#!/usr/bin/env python3
"""Compile the two furnished Shacraft station floors without world I/O.
The caller owns the exterior, authoritative survey and expected-state writes.
This module owns only the two-block-eroded footprint at block Y 98..123.
All six SMASH panels are decorative, unassigned slots. Functional signs, text
displays and the individual lift transition are deliberately metadata only.
"""
from collections import Counter, deque
AIR = "minecraft:air"
CREAM = "minecraft:smooth_sandstone"
PALE = "minecraft:smooth_quartz"
CUT = "minecraft:cut_sandstone"
GREEN = "minecraft:waxed_oxidized_cut_copper"
GOLD = "minecraft:gold_block"
WOOD = "minecraft:spruce_planks"
DARK = "minecraft:dark_oak_planks"
CHAIN = "minecraft:iron_chain[axis=y,waterlogged=false]"
LANTERN = "minecraft:lantern[hanging=true,waterlogged=false]"
LEAF = "minecraft:spruce_leaves[distance=7,persistent=true,waterlogged=false]"
LOG = "minecraft:spruce_log[axis=y]"
def _slab(material, top=False):
return f"minecraft:{material}[type={'top' if top else 'bottom'},waterlogged=false]"
def _stair(material, facing, top=False):
return (f"minecraft:{material}[facing={facing},half={'top' if top else 'bottom'},"
"shape=straight,waterlogged=false]")
def _rect(box):
a, b, c, d = map(int, box)
return {(x, z) for x in range(a, b + 1) for z in range(c, d + 1)}
def _pose(x, y, z, text, facing="south", scale=0.7, **extra):
return {"position": {"x": x, "y": y, "z": z}, "text": text,
"facing": facing, "scale": scale, **extra}
class _Interior:
def __init__(self, footprint, layout):
self.footprint = {(int(x), int(z)) for x, z in footprint}
self.inside = {(x, z) for x, z in self.footprint
if all((x + dx, z + dz) in self.footprint
for dx in range(-2, 3) for dz in range(-2, 3))}
if not self.inside:
raise ValueError("The station needs an interior after its two-block setback")
self.layout = layout
self.states = {}
self.groups = {}
self.features = []
self.texts = []
self.targets = {99: [], 113: []}
self.selections = []
self.lift = {}
self.floor_walk = {}
def put(self, x, y, z, state, group):
p = (int(x), int(y), int(z))
if (p[0], p[2]) not in self.inside or not 98 <= p[1] <= 123:
raise ValueError(f"Interior write outside the owned volume: {p} ({group})")
self.states[p] = state
self.groups[p] = group
def box(self, bounds, lo, hi, state, group, clipped=False):
for x, z in sorted(_rect(bounds)):
if clipped and (x, z) not in self.inside:
continue
for y in range(lo, hi + 1):
self.put(x, y, z, state, group)
def target(self, feet, x, z, name):
self.targets[feet].append({"name": name, "x": x, "y": feet, "z": z})
def record(self, kind, bounds, feet, **extra):
self.features.append({"kind": kind, "bounds_xz": list(bounds),
"walk_y": feet, **extra})
def shell(self):
for x, z in sorted(self.inside):
for y in range(98, 124):
if y in (98, 112):
state, group = CREAM, "interior:continuous-floor"
elif y == 111:
state, group = "minecraft:stone_bricks", "interior:solid-intermediate-deck"
else:
state, group = AIR, "interior:owned-room-air"
self.put(x, y, z, state, group)
def floors(self, feet):
floor = feet - 1
group = f"interior:{feet}:floor-inlay"
# Large calm limestone panels, outlined by the architectural column grid.
for x, z in sorted(self.inside):
if x in (-51, -46, -21, -16, 15, 20) or z in (-129, -124, -106, -101):
self.put(x, floor, z, PALE, group)
if x in (-50, -17, 19) and -135 <= z <= -93:
self.put(x, floor, z, CUT, group)
for x in (-11, -10, -2, -1):
for z in range(-115, -84):
if (x, z) in self.inside:
self.put(x, floor, z, GREEN if x in (-10, -2) else PALE, group)
for z in (-111, -103, -95, -87):
for x in (-10, -2):
if (x, z) in self.inside:
self.put(x, floor, z, GOLD, group)
centres = [(-59, -117), (25, -117), (-34, -103), (5, -99)]
if feet == 99:
centres += [(-33, -117), (-31, -135), (5, -135)]
for cx, cz in centres:
for dx in range(-5, 6):
for dz in range(-5, 6):
if (cx + dx, cz + dz) not in self.inside:
continue
r = abs(dx) + abs(dz)
if r == 5:
state = GREEN
elif r == 4:
state = PALE
elif r <= 2 and (dx == 0 or dz == 0):
state = GOLD if r == 0 else CUT
else:
continue
self.put(cx + dx, floor, cz + dz, state, group)
if feet == 113:
# A terracotta runner belongs to SMASH, while the centre axis stays pale.
for x, z in sorted(_rect([-46, 11, -134, -130])):
self.put(x, floor, z, "minecraft:orange_terracotta", group)
for x in range(-46, 12):
for z in (-135, -129):
self.put(x, floor, z, PALE if x % 10 else GOLD, group)
for x, z in sorted(_rect([22, 29, -124, -112])):
edge = x in (22, 29) or z in (-124, -112)
self.put(x, floor, z, GREEN if edge else "minecraft:orange_terracotta", group)
def arcade(self, feet, ceiling):
group = f"interior:{feet}:arcade"
panel_y = ceiling - 1
# A solid coffer ceiling closes the upper floor below the inaccessible attic.
for x, z in sorted(self.inside):
self.put(x, panel_y, z, WOOD, group)
if z in (-139, -128, -125, -105, -102, -91) or x in (-50, -47, -20, -17, 16, 19):
self.put(x, panel_y, z, DARK, group)
if z in (-128, -125, -105, -102):
self.put(x, panel_y - 1, z, CREAM, group)
for bounds in self.layout["aligned_columns"]:
a, b, c, d = bounds
self.box(bounds, feet, feet, CUT, group)
self.box(bounds, feet + 1, feet + 1, PALE, group)
self.box(bounds, feet + 2, ceiling - 4,
"minecraft:quartz_pillar[axis=y]", group)
self.box(bounds, ceiling - 3, ceiling - 1, CREAM, group)
self.box([a - 1, b + 1, c - 1, d + 1], ceiling - 3, ceiling - 3,
_slab("smooth_sandstone_slab", top=True), group, clipped=True)
self.box([a - 1, b + 1, c - 1, d + 1], ceiling - 2, ceiling - 2,
CREAM, group, clipped=True)
self.record("aligned-column", bounds, feet)
# High corbels make the two column rows read as a shallow cream arcade.
for bounds in self.layout["aligned_columns"]:
a, b, c, d = bounds
for x, facing in ((a - 2, "east"), (b + 2, "west")):
for z in (c, d):
if (x, z) in self.inside:
self.put(x, ceiling - 3, z,
_stair("smooth_sandstone_stairs", facing, top=True), group)
def chandelier(self, x, z, feet, ceiling):
group = f"interior:{feet}:chandelier"
collar = ceiling - 4
for y in range(collar + 1, ceiling):
self.put(x, y, z, CHAIN, group)
self.put(x, collar, z, GOLD, group)
for dx, dz in ((-1, 0), (1, 0), (0, -1), (0, 1)):
self.put(x + dx, collar, z + dz, GREEN, group)
self.put(x + dx, collar - 1, z + dz, LANTERN, group)
self.record("four-lantern-chandelier", [x - 1, x + 1, z - 1, z + 1],
feet, lowest_block_y=collar - 1)
def bench(self, bounds, feet, facing="south", name="bench"):
a, b, c, d = bounds
if b - a < 3 or d - c != 1:
raise ValueError("Station benches use a long, two-block-deep rectangle")
group = f"interior:{feet}:bench"
back_z, seat_z = (c, d) if facing == "south" else (d, c)
stair_facing = "north" if facing == "south" else "south"
for x in range(a, b + 1):
if x in (a, b):
for z in (c, d):
self.put(x, feet, z, CUT, group)
self.put(x, feet + 1, z, _slab("smooth_sandstone_slab"), group)
else:
self.put(x, feet, seat_z, _stair("spruce_stairs", stair_facing), group)
self.put(x, feet, back_z, WOOD, group)
self.put(x, feet + 1, back_z, _slab("spruce_slab"), group)
front = d + 1 if facing == "south" else c - 1
for x in range(a + 1, b):
self.target(feet, x, front, f"{name}-access-{x}")
self.record("spruce-bench", bounds, feet, facing=facing)
def planter(self, x, z, feet, name="topiary"):
group = f"interior:{feet}:planter"
bounds = [x - 1, x + 1, z - 1, z + 1]
self.box(bounds, feet, feet, CUT, group)
self.put(x, feet, z, "minecraft:dirt", group)
self.put(x, feet + 1, z, LOG, group)
self.put(x, feet + 2, z, LOG, group)
for dx, dz in ((-1, 0), (0, -1), (0, 0), (0, 1), (1, 0)):
self.put(x + dx, feet + 3, z + dz, LEAF, group)
self.put(x, feet + 4, z, LEAF, group)
self.record(name, bounds, feet)
def lift_cabin(self, feet, ceiling):
group = f"interior:{feet}:lift"
housing = self.layout["lift"]["housing"]
cabin = self.layout["lift"]["clear_cabin"]
inner = _rect(cabin)
a, b, c, d = housing
for x, z in sorted(_rect(housing)):
if (x, z) in inner:
self.put(x, feet - 1, z, "minecraft:polished_andesite", group)
continue
for y in range(feet, ceiling):
state = GREEN
if x in (a, b) and z in (c, d):
state = CUT if y in (feet, feet + 5) else CREAM
if y == feet + 5 and z == d:
state = GOLD
self.put(x, y, z, state, group)
# The unused volume above each cabin is solid, never an open shaft.
self.box(cabin, feet + 7, ceiling - 1, WOOD, group)
self.box(cabin, feet, feet + 6, AIR, group)
self.box([-8, -4, -117, -116], feet, feet + 4, AIR, group)
self.box([-8, -4, -117, -116], feet + 5, feet + 5, GOLD, group)
for x in (-8, -4):
for z in range(-122, -117):
self.put(x, feet - 1, z, GREEN, group)
self.put(-6, feet + 6, -120, CHAIN, group)
self.put(-6, feet + 5, -120, LANTERN, group)
selector = (-6, feet + 2, -123)
self.put(*selector, GOLD, group)
number = 1 if feet == 99 else 2
self.lift[str(number)] = {
"housing_bounds_xz": housing, "clear_cabin_bounds_xz": cabin,
"walk_y": feet, "door_bounds_xz": [-8, -4, -117, -116],
"door_clear_height": 5, "selector_block": list(selector),
"destination": {"x": -5.5, "y": 113 if feet == 99 else 99,
"z": -119.5, "yaw": 0, "pitch": 0},
"landing_block": [-6, feet, -120], "floor_is_solid": True,
}
self.texts.append(_pose(-5.5, feet + 6.2, -114.93,
"1 ВЕСТИБЮЛЬ" if number == 1 else "2 SMASH",
scale=0.9, id=f"lift-{number}-front"))
self.texts.append(_pose(-5.5, feet + 3.3, -121.92,
"Вверх · SMASH" if number == 1 else "Вниз · Вестибюль",
scale=0.45, id=f"lift-{number}-selector"))
self.target(feet, -6, -120, f"lift-{number}-landing")
self.target(feet, -6, -114, f"lift-{number}-approach")
self.record("enclosed-lift-cabin", housing, feet, floor_number=number)
def gallery_panel(self, x, z, feet, title, subtitle, facing="east"):
group = f"interior:{feet}:gallery-panel"
# East-facing panels stand along the western wall, clear of the arcade.
for dz in range(-2, 3):
for dy in range(0, 6):
state = CUT if abs(dz) == 2 or dy in (0, 5) else DARK
self.put(x, feet + dy, z + dz, state, group)
for dz in (-2, 2):
self.put(x, feet + 2, z + dz, GOLD, group)
self.texts.append(_pose(x + 1.04, feet + 3.9, z + 0.5,
title + "\n" + subtitle, facing=facing, scale=0.52,
id=f"gallery-{feet}-{x}-{z}"))
self.target(feet, x + 3, z, title)
self.record("rules-panel" if feet == 113 else "station-gallery-panel",
[x, x, z - 2, z + 2], feet)
def diorama(self):
feet = 113
a, b, c, d = self.layout["smash_display"]
group = "interior:113:contained-island-diorama"
self.box([a, b, c, d], feet, feet, "minecraft:black_concrete", group)
for x, z in sorted(_rect([a, b, c, d])):
if x in (a, b) or z in (c, d):
self.put(x, feet, z, CUT, group)
for y in (feet + 1, feet + 2):
self.put(x, y, z, "minecraft:glass", group)
self.put(x, feet + 3, z, _slab("smooth_sandstone_slab"), group)
# Miniatures hover above an opaque, intact display base, inside a glass case.
for ix, (cx, cz, radius) in enumerate(((-38, -115, 2), (-29, -115, 2), (-34, -120, 2))):
self.put(cx, 114, cz, "minecraft:deepslate[axis=y]", group)
for dx in range(-radius, radius + 1):
for dz in range(-radius, radius + 1):
dist = abs(dx) + abs(dz)
if dist <= 2:
self.put(cx + dx, 115, cz + dz, "minecraft:stone", group)
if dist <= 3:
self.put(cx + dx, 116, cz + dz, "minecraft:moss_block", group)
self.put(cx, 117, cz, LOG, group)
self.put(cx, 118, cz, LOG, group)
for dx, dz in ((-1, 0), (0, -1), (0, 0), (0, 1), (1, 0)):
self.put(cx + dx, 119, cz + dz, LEAF, group)
self.put(cx, 120, cz, LEAF, group)
self.put(cx + (1 if ix != 1 else -1), 117, cz + 1,
"minecraft:red_concrete" if ix == 1 else "minecraft:light_blue_concrete", group)
self.texts.append(_pose((a + b) / 2 + 0.5, 115, d + 1.05,
"SMASH · УДЕРЖИСЬ НА ОСТРОВЕ", scale=0.6,
id="smash-diorama-caption"))
self.target(feet, -33, d + 2, "diorama-south-view")
self.target(feet, a - 2, -117, "diorama-west-view")
self.target(feet, b + 2, -117, "diorama-east-view")
self.record("closed-display-only-diorama", [a, b, c, d], feet,
enclosed=True, arena=False, floor_intact=True, top_y=120)
def selection_bays(self):
feet = 113
group = "interior:113:smash-selection-bays"
# Solid infill behind the recessed panels prevents unfinished rear rooms.
for x, z in sorted(self.inside):
if -47 <= x <= 13 and z <= -140:
for y in range(feet, 124):
self.put(x, y, z, CREAM, group)
for index, bounds in enumerate(self.layout["smash_selection_bays"], 1):
a, b, c, d = bounds
mid = (a + b) // 2
for x in range(a, b + 1):
for y in range(feet, feet + 8):
self.put(x, y, c, DARK, group)
for x in (a, b):
for z in range(c + 1, d + 1):
for y in range(feet, feet + 7):
self.put(x, y, z, CUT if y == feet else CREAM, group)
self.box([a, b, c + 1, d], feet + 7, feet + 7, CREAM, group)
self.box([a + 1, b - 1, c + 1, c + 1], feet + 1, feet + 6, GOLD, group)
# Five-wide miniature reliefs are explicitly generic, unassigned previews.
for dx in range(-2, 3):
for dy in range(4):
material = "light_blue_concrete"
summit = 1 + ((dx + index) % 3 == 0)
if dy < summit:
material = "stone" if dy == 0 else "moss_block"
if dy == 3 and dx == (index % 3) - 1:
material = "white_concrete"
self.put(mid + dx, feet + 2 + dy, c + 2,
"minecraft:" + material, group)
for x in range(mid - 1, mid + 2):
self.put(x, feet, d, CUT, group)
self.put(x, feet + 1, d, WOOD, group)
self.put(mid, feet + 6, c + 2, LANTERN, group)
self.put(mid, feet + 7, c + 2, GREEN, group)
sign = [mid, feet + 1, d + 1]
support = [mid, feet + 1, d]
self.selections.append({
"slot": f"{index:02d}", "bounds_xz": bounds,
"assigned": False, "destination": None,
"relief_is_decorative": True,
"sign_block": sign, "sign_support_block": support,
"sign_facing": "south", "sign_lines": [f"АРЕНА {index:02d}", "Не назначено", "", ""],
"standing_block": [mid, feet, d + 2],
"label_pose": _pose(mid + 0.5, feet + 6.6, d + 1.05,
f"{index:02d}", scale=0.55),
})
self.target(feet, mid, d + 2, f"arena-{index:02d}-sign")
self.record("unassigned-arena-selection-bay", bounds, feet, slot=index)
self.box([-35, 1, -139, -138], 121, 123, DARK, group)
self.texts.append(_pose(-16.5, 122.2, -136.94, "S M A S H", scale=2.2,
id="smash-header"))
def furniture(self, feet):
for i, bounds in enumerate(self.layout["wing_benches"]):
self.bench(bounds, feet, "south", f"wing-bench-{i + 1}")
for x, z in ((-56, -131), (-56, -106), (21, -131), (21, -106)):
self.planter(x, z, feet)
# The north and south galleries are furnished public rooms, without doors
# suggesting additional unfinished wings or upper-floor circulation.
for i, (bounds, facing) in enumerate((([-22, -15, -94, -93], "north"),
([0, 7, -94, -93], "north"))):
self.bench(bounds, feet, facing, f"south-gallery-bench-{i + 1}")
for x, z in ((-30, -94), (12, -94)):
if _rect([x - 1, x + 1, z - 1, z + 1]) <= self.inside:
self.planter(x, z, feet)
if feet == 99:
for i, bounds in enumerate(([-44, -37, -137, -136], [-26, -19, -137, -136],
[1, 8, -137, -136])):
self.bench(bounds, feet, "south", f"north-gallery-bench-{i + 1}")
for x in (-31, 11):
self.planter(x, -137, feet)
self.gallery_panel(-72, -116, feet, "SHACRAFT", "Площадь прибытия")
self.texts.append(_pose(-5.5, 108, -114.93,
"SHACRAFT", scale=0.9,
id="vestibule-heading"))
else:
for z, title, subtitle in ((-122, "УРОН", "Больше урона — сильнее отбрасывание"),
(-115, "ОТБРАСЫВАНИЕ", "Удержись на острове"),
(-108, "ДВОЙНОЙ ПРЫЖОК", "Вернись на площадку")):
self.gallery_panel(-72, z, feet, title, subtitle)
self.texts.append(_pose(26, 118.5, -105.9, "ЗОНА ОЖИДАНИЯ", scale=0.7,
id="smash-waiting-heading"))
def leaf_distances(self):
leaves = {p for p, s in self.states.items() if s.startswith("minecraft:spruce_leaves[")}
distance = {}
queue = deque()
for p, s in self.states.items():
if s.startswith("minecraft:spruce_log["):
queue.append((p, 0))
while queue:
(x, y, z), d = queue.popleft()
if d >= 6:
continue
for p in ((x - 1, y, z), (x + 1, y, z), (x, y - 1, z),
(x, y + 1, z), (x, y, z - 1), (x, y, z + 1)):
if p in leaves and d + 1 < distance.get(p, 7):
distance[p] = d + 1
queue.append((p, d + 1))
for p in leaves:
self.states[p] = (f"minecraft:spruce_leaves[distance={distance.get(p, 7)},"
"persistent=true,waterlogged=false]")
def navigation(self):
details = {}
for feet, seed in ((99, (-6, -86)), (113, (-6, -120))):
clear = {(x, z) for x, z in self.inside
if all(self.states[(x, y, z)] == AIR for y in range(feet, feet + 4))}
if seed not in clear:
raise ValueError(f"Interior navigation start is obstructed at {feet}: {seed}")
reached = {seed}
queue = deque([seed])
while queue:
x, z = queue.popleft()
for p in ((x - 1, z), (x + 1, z), (x, z - 1), (x, z + 1)):
if p in clear and p not in reached:
reached.add(p)
queue.append(p)
for target in self.targets[feet]:
if (target["x"], target["z"]) not in reached:
raise ValueError(f"Unreachable interior target: {target}")
# Close genuine inaccessible air pockets; never leave a half-built room.
sealed = clear - reached
for x, z in sorted(sealed):
for y in range(feet, 111 if feet == 99 else 124):
self.put(x, y, z, CREAM, f"interior:{feet}:sealed-service-infill")
reserved = [self.layout["reserved_clear_aisles"]["entry"]]
if feet == 113:
reserved.append(self.layout["reserved_clear_aisles"]["bay_front"])
for bounds in reserved:
required = _rect(bounds) & self.inside
blocked = required - reached
if blocked:
raise ValueError(f"Reserved circulation is blocked at Y{feet}: {sorted(blocked)[:5]}")
self.floor_walk[feet] = [{"x": x, "y": feet, "z": z}
for x, z in sorted(reached)]
details[str(feet)] = {"walk_columns": len(reached),
"minimum_verified_headroom": 4,
"named_targets": self.targets[feet],
"sealed_inaccessible_columns": len(sealed),
"all_named_targets_reachable": True,
"reserved_aisles_clear": True}
return details
def compile_interior(footprint, layout):
"""Return canonical voxel states, per-voxel groups, and JSON-safe metadata.
``footprint`` contains the actual surveyed foundation's (x, z) columns.
Geometry is deterministic and absolute, as specified by the approved layout.
The returned air states are intentional room clearance; callers must apply
the normal expected-state and protected-volume checks before any live write.
"""
b = _Interior(footprint, layout)
floors = [(int(f["walk_y"]), int(f["ceiling_underside_y"])) for f in layout["floors"]]
if floors != [(99, 111), (113, 124)]:
raise ValueError("This approved interior is defined only for walk Y99 and Y113")
b.shell()
for feet, ceiling in floors:
b.floors(feet)
b.arcade(feet, ceiling)
b.furniture(feet)
lights = [(-58, -116), (25, -116), (5, -107), (-32, -101)]
if feet == 99:
lights += [(-33, -116), (-30, -132)]
for x, z in lights:
b.chandelier(x, z, feet, ceiling)
b.lift_cabin(feet, ceiling)
b.diorama()
b.selection_bays()
b.leaf_distances()
navigation = b.navigation()
palette = sorted({s.partition("[")[0] for s in b.states.values()})
metadata = {
"schema_version": 1,
"name": "Shacraft station furnished vestibule and SMASH gallery",
"status": "compiled candidate; requires caller survey, collision and live verification",
"owned_y": [98, 123], "perimeter_setback_blocks": 2,
"foundation_columns": len(b.footprint), "interior_columns": len(b.inside),
"state_count": len(b.states), "non_air_count": sum(s != AIR for s in b.states.values()),
"group_counts": dict(sorted(Counter(b.groups.values()).items())),
"palette": palette, "features": b.features, "lift": b.lift,
"selection_bays": b.selections, "text_displays": b.texts,
"walk_points": b.floor_walk[99] + b.floor_walk[113],
"walk_points_by_floor": {str(k): v for k, v in b.floor_walk.items()},
"navigation": navigation,
"design_invariants": {
"only_two_furnished_floors": True, "upper_floor_has_no_holes": True,
"lift_has_no_open_shaft": True, "no_attic_or_clocktower_access": True,
"diorama_is_closed_and_display_only": True, "arena_slots_are_unassigned": True,
"functional_text_signs_and_lift_are_caller_owned": True,
},
}
return b.states, b.groups, metadata
@@ -0,0 +1,19 @@
import com.google.gson.JsonParser;
import io.github.minecraftbuilder.core.TerrainRecipe;
import java.io.*;
import java.nio.file.*;
/** Offline composition experiment, not a world writer or a production recipe. */
public final class NaturalTerrainStudy {
private static final io.github.minecraftbuilder.terrainworld.NaturalTerrain terrain =
new io.github.minecraftbuilder.terrainworld.NaturalTerrain(28092005);
static float height(int x,int z) { return terrain.height(x,z); }
public static void main(String[] args) throws Exception {
var old=new TerrainRecipe(JsonParser.parseString(Files.readString(Path.of(args[0]))).getAsJsonObject());
Path output=Path.of(args[1]);Files.createDirectories(output);
try(var a=new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(output.resolve("before.f32"))));
var b=new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(output.resolve("natural.f32"))))) {
for(int z=-384;z<384;z++)for(int x=-384;x<384;x++) {a.writeFloat(old.surfaceHeight(x,z));b.writeFloat(height(x,z));}
}
}
}
+46
View File
@@ -0,0 +1,46 @@
# Shacraft terrain composition study
This is an **offline prototype** responding to the first lobby terrain's rectangular platforms and repetitive slopes. The original study did not alter a live world. Its exact height field is now available through the production `shacraft-natural-v1` initial-world profile; it still does not extend the production MCP recipe schema.
## Findings and design decisions
The original recipe combines three correlated octaves of grid-based value noise, then flattens large rectangular areas. It also paints grass on every exposed top step regardless of slope. The screenshots show both geometric platform edges and repetitive green/brown contour bands. Adding more octaves alone cannot remove those platform boundaries.
[Red Blob Games](https://www.redblobgames.com/maps/terrain-from-noise/) explains independent octave sampling, Simplex noise as a way to reduce directional artifacts, ridged transformations, and elevation redistribution. The study uses seeded OpenSimplex2S with distinct fields for broad relief, ridges, coordinate warping and fine detail.
[libnoise tutorial 5](https://libnoise.sourceforge.net/tutorials/tutorial5.html) separates the terrain-type control map from mountain and lowland height fields. Here explicit curved mountain corridors provide artistic control: north is the main skyline, side ranges are lower, and the central valley stays comparatively calm. Mountain detail is weighted by those corridors instead of covering the entire map uniformly.
[FastNoiseLite's documentation](https://github.com/Auburn/FastNoiseLite/wiki/Documentation) provides fBm, ridged fractals, octave weighting and domain warping. The prototype uses two independent low-frequency fields to displace X/Z coordinates before evaluating terrain, producing irregular ridge paths and water margins. This is a modest 27-block warp, not unlimited distortion.
There are no rectangular plateau features in the study. Broad smooth hills leave space for architecture without committing to enormous flat pads. Later construction should flatten only the actual footprint and blend the foundation into the slope. Natural terrain alone does not guarantee walkable routes; those need a separate route/grade pass after the silhouette is selected.
The lakes and connected river paths share Y=48. Shorelines arise from the intersection between the computed ground and that water plane. River cuts are curved and variable in width. The shape is authored and noise-modulated; it is **not** a drainage or hydraulic erosion simulation. Small remaining angular changes around path vertices should be replaced by spline sampling in a production implementation.
## Prototype parameters
- Broad lowland field: scale 220 blocks, amplitude 11, 4 octaves.
- Mountain ridge field: scale 85, 5 octaves, gain 0.48, lacunarity 2.07, weighted strength 0.7.
- Warp fields: scale 165, 3 octaves, displacement 27 blocks.
- Fine detail: scale 17, amplitude about 1.2–4.4 depending on mountain influence.
- Comparison palette: slope-dependent rock/grass and depth-dependent water, **identical for both versions**. This isolates geometry; it is not a Minecraft material or shader preview.
## Reproduce
From the repository root, with the project Java 25 runtime on PATH:
```bash
mkdir -p .runtime/terrain-study
javac -cp terrain-world-plugin/target/terrain-world-plugin-0.1.0-SNAPSHOT.jar -d .runtime/terrain-study \
scripts/terrain-study/NaturalTerrainStudy.java
java -cp .runtime/terrain-study:terrain-world-plugin/target/terrain-world-plugin-0.1.0-SNAPSHOT.jar \
NaturalTerrainStudy examples/terrain/shacraft-lobby-world.json .runtime/terrain-study
python3 scripts/terrain-study/render.py
```
Rendering requires numpy and matplotlib. The binary samples are 768×768 big-endian float32, row-major Z then X. No upsampling, erosion or post-sculpting is hidden in the renderer. Perspective views sample every fourth column and use true vertical scale.
Outputs: `docs/references/shacraft-terrain-study-plan.png`, `shacraft-terrain-study-perspective.png`, and `shacraft-terrain-study.json`. These are computed previews, **not screenshots**. The original comparison images predate the version 2 world. The production height field is tested against the SHA-256 of all 589,824 original prototype float samples.
## Third-party source
`../../terrain-world-plugin/src/main/java/io/github/minecraftbuilder/terrainworld/noise/FastNoiseLite.java` is upstream Java source with only a package declaration added from [Auburn/FastNoiseLite](https://github.com/Auburn/FastNoiseLite), pinned to commit `785f37a9ad76e283586a379675085f2063ae03f7`. Its MIT license and copyright notice are preserved at the top of the file. The MIT notice is also bundled in the production jar under `META-INF/LICENSE-FastNoiseLite.txt`.
+63
View File
@@ -0,0 +1,63 @@
"""Render comparable scientific previews from sampled height fields; never edits screenshots."""
from pathlib import Path
import json
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.colors import LightSource
ROOT = Path(__file__).resolve().parents[2]
DATA = ROOT / '.runtime/terrain-study'
OUT = ROOT / 'docs/references'
WATER = 48
fields = [np.fromfile(DATA / name, dtype='>f4').reshape(768, 768).astype(float)
for name in ['before.f32', 'natural.f32']]
names = ['V1 — rectangular platforms', 'Study — ridges, soft hills, winding water']
def colors(ground):
dz, dx = np.gradient(ground)
slope = np.hypot(dx, dz)
rock = np.clip((slope - .55) / 1.5, 0, 1)[..., None]
green = np.array([.35, .46, .25])
stone = np.array([.54, .54, .49])
rgb = green * (1-rock) + stone * rock
# Identical materials and lighting for both fields: isolate the shape comparison.
shaded = LightSource(315, 42).shade_rgb(rgb, ground, vert_exag=1, blend_mode='soft')
depth = np.clip((WATER-ground)/25, 0, 1)[..., None]
water = np.array([.19, .46, .50])*(1-depth) + np.array([.12, .30, .37])*depth
return np.where((ground < WATER)[..., None], water, shaded)
fig, axes = plt.subplots(1, 2, figsize=(16, 8), facecolor='#f4f3ee')
for ax, ground, name in zip(axes, fields, names):
ax.imshow(colors(ground), extent=(-384,384,384,-384))
ax.set_title(name, fontsize=16, loc='left', pad=16)
ax.set_xlabel('X / blocks'); ax.set_ylabel('Z / blocks — north up')
fig.suptitle('Shacraft / terrain composition study · 768 × 768 blocks', fontsize=20, x=.06, ha='left', y=.99)
fig.text(.06,.025,'Computed height fields. Same scale, water level and shading. Right: offline prototype, not yet in Minecraft.',fontsize=11)
fig.subplots_adjust(left=.06,right=.98,top=.90,bottom=.10,wspace=.18)
fig.savefig(OUT/'shacraft-terrain-study-plan.png',dpi=130)
plt.close(fig)
fig=plt.figure(figsize=(16,8),facecolor='#f4f3ee')
for i,(ground,name) in enumerate(zip(fields,names)):
ax=fig.add_subplot(1,2,i+1,projection='3d',facecolor='#f4f3ee')
s=4; x=np.arange(-384,384,s); z=np.arange(-384,384,s); xx,zz=np.meshgrid(x,z)
ax.plot_surface(xx,zz,np.maximum(ground[::s,::s],WATER),facecolors=colors(ground)[::s,::s],
rstride=1,cstride=1,linewidth=0,antialiased=False,shade=False)
ax.set(xlim=(-384,384),ylim=(-384,384),zlim=(25,220))
ax.set_box_aspect((768,768,195));ax.view_init(elev=37,azim=-58);ax.set_axis_off()
ax.set_title(name,loc='left',fontsize=16,pad=0)
fig.suptitle('Shacraft / compare silhouettes before rebuilding',fontsize=20,x=.05,ha='left',y=.95)
fig.text(.05,.07,'Geometric preview at true vertical scale. No rectangular plateaus in the new study. No erosion simulation.',fontsize=11)
fig.subplots_adjust(left=.01,right=.99,top=.86,bottom=.10,wspace=-.06)
fig.savefig(OUT/'shacraft-terrain-study-perspective.png',dpi=130)
plt.close(fig)
stats=[]
for ground,name in zip(fields,names):
dz,dx=np.gradient(ground);s=np.hypot(dx,dz)
stats.append({'name':name,'min_ground_y':float(ground.min()),'max_ground_y':float(ground.max()),
'water_columns':int((ground<WATER).sum()),'slope_p95':float(np.percentile(s,95))})
(OUT/'shacraft-terrain-study.json').write_text(json.dumps({'world_applied':False,'water_level':WATER,'fields':stats},indent=2)+'\n')
print(json.dumps(stats))
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""Offline heightmap previews and bounded, resumable terrain batches. Uses the normal checked RPC editor."""
import argparse
import fcntl
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import time
import urllib.error
import urllib.request
import uuid
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('mcb_launcher', ROOT / 'scripts/bridge.py')
launcher = importlib.util.module_from_spec(spec)
spec.loader.exec_module(launcher)
class RpcError(RuntimeError):
def __init__(self, code, message):
super().__init__(f'{code}: {message}')
self.code = code
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
raise RpcError('redirect_rejected', 'Backend redirects are not permitted')
class Backend:
def __init__(self, config, console=False):
text = config.read_text()
self.token = launcher.scalar(text, 'agent-token')
self.scope = {'player_id': 'console' if console else launcher.scalar(text, 'owner-uuid'),
'project_id': launcher.scalar(text, 'project-id')}
port = int(launcher.scalar(text, 'http-port'))
if not 1 <= port <= 65535 or not self.token or not self.scope['player_id']:
raise ValueError('Invalid backend configuration or unbound owner')
self.url = f'http://127.0.0.1:{port}/v1/rpc'
self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
def call(self, method, **params):
body = json.dumps({'method': method, 'params': {**params, **self.scope}, 'requestId': str(uuid.uuid4())}).encode()
for _ in range(100):
request = urllib.request.Request(self.url, data=body, headers={'Content-Type': 'application/json', 'Authorization': 'Bearer ' + self.token})
try:
with self.opener.open(request, timeout=30) as response:
data = json.load(response)
except urllib.error.HTTPError as error:
data = json.load(error)
if data.get('ok'):
return data['result']
failure = data.get('error', {})
if failure.get('code') == 'busy':
time.sleep(.1)
continue
raise RpcError(failure.get('code', 'invalid_response'), failure.get('message', 'Backend rejected request').replace(self.token, '[REDACTED]'))
raise RpcError('busy', 'Backend did not become ready')
def save(path, value):
temporary = path.with_suffix(path.suffix + '.tmp')
with temporary.open('w') as f:
json.dump(value, f, indent=2)
f.flush()
os.fsync(f.fileno())
temporary.replace(path)
directory = os.open(path.parent, os.O_RDONLY)
try:
os.fsync(directory)
finally:
os.close(directory)
def finish(backend, operation):
deadline = time.monotonic() + 180
while time.monotonic() < deadline:
result = backend.call('operation_status', operation_id=operation)
if result['status'] in ('applied', 'conflict', 'failed', 'cancelled', 'recovery_required'):
return result
time.sleep(.15)
raise RuntimeError(f'Operation still running: {operation}; resume with the same manifest')
def apply_plan(backend, entry, manifest, path):
# Persist plan and stable key BEFORE transmission. Unknown outcomes reuse this exact request.
entry.setdefault('idempotency_key', 'terrain-' + str(uuid.uuid4()))
save(path, manifest)
if 'operation_id' not in entry:
result = backend.call('build_apply', plan_id=entry['plan_id'], plan_hash=entry['plan_hash'], idempotency_key=entry['idempotency_key'])
entry['operation_id'] = result['operation_id']
save(path, manifest)
result = finish(backend, entry['operation_id'])
entry['status'] = result['status']
entry['written'] = result['written']
save(path, manifest)
if result['status'] != 'applied':
raise RuntimeError(f"Stopped on {result['status']}: {entry['operation_id']}. Inspect conflicts; no new plan or blind retry was created.")
def run_batch(args):
backend = Backend(args.config, args.console)
context = backend.call('project_context')
scope = {key: context[key] for key in ('project_id', 'world_id', 'world_epoch')}
path = args.manifest.resolve()
path.parent.mkdir(parents=True, exist_ok=True)
with path.with_suffix(path.suffix + '.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
manifest = json.loads(path.read_text()) if path.exists() else None
if manifest and manifest['scope'] != scope:
raise RuntimeError('Manifest belongs to another project/world/epoch')
if args.command == 'undo':
if manifest is None:
raise RuntimeError('Manifest not found')
# Resolve an apply whose response was lost BEFORE assuming there is nothing to undo.
for entry in reversed(manifest['tiles']):
if 'idempotency_key' in entry and 'operation_id' not in entry:
raise RuntimeError('Uncertain apply response. Resume the original apply with this manifest to resolve its stable key before undo; undo will not start an unconfirmed write.')
if 'operation_id' not in entry:
continue
result = finish(backend, entry['operation_id'])
if result['status'] == 'recovery_required':
raise RuntimeError('Interrupted server operation requires recovery review first')
if not result['written']:
continue
if 'undo' not in entry:
entry['undo'] = backend.call('operation_undo_prepare', operation_id=entry['operation_id'])
save(path, manifest)
apply_plan(backend, entry['undo'], manifest, path)
manifest['undone'] = True; save(path, manifest)
print(json.dumps({'status': 'undone', 'manifest': str(path)}))
return
recipe = json.loads(args.recipe.read_text())
preview = backend.call('terrain_preview', recipe=recipe, resolution=64)
terrain_id = preview['terrain_id']
if manifest:
if manifest['terrain_id'] != terrain_id or manifest['start'] != args.start or manifest['count'] != args.count or manifest['tile_budget'] != preview['tile_budget']:
raise RuntimeError('Recipe or tile range differs from saved batch')
if manifest.get('undone') or any('undo' in entry for entry in manifest['tiles']):
raise RuntimeError('Batch has begun undo; finish undo and use a NEW manifest for new work')
else:
if args.start < 0 or not 1 <= args.count <= 64 or args.start + args.count > preview['tile_count']:
raise RuntimeError('Choose 1..64 existing tiles per batch')
manifest = {'version': 1, 'scope': scope, 'terrain_id': terrain_id, 'recipe': recipe,
'start': args.start, 'count': args.count, 'tile_budget': preview['tile_budget'], 'tiles': []}
save(path, manifest)
# The entire requested envelope must fit; never silently clip to an unrelated current project.
for axis in ('x', 'y', 'z'):
if recipe['min'][axis] < context['region']['min'][axis] or recipe['max'][axis] > context['region']['max'][axis]:
raise RuntimeError('Recipe exceeds selected project area; choose a dedicated terrain site first')
for index in range(args.start, args.start + args.count):
entry = next((e for e in manifest['tiles'] if e['tile_index'] == index), None)
if entry is None:
entry = backend.call('terrain_prepare', terrain_id=terrain_id, tile_index=index)
manifest['tiles'].append(entry)
save(path, manifest)
if entry.get('status') == 'empty' or entry.get('changed_blocks') == 0:
continue
if entry.get('status') in ('conflict', 'cancelled', 'failed', 'recovery_required'):
raise RuntimeError('Batch stopped previously; inspect or undo it instead of forcing through')
apply_plan(backend, entry, manifest, path)
print(json.dumps({'tile': index, 'status': entry['status'], 'written': entry['written']}), flush=True)
print(json.dumps({'status': 'completed', 'manifest': str(path), 'tiles': len(manifest['tiles'])}))
def main():
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest='command', required=True)
preview = sub.add_parser('preview', help='Render the exact height field OFFLINE; no server or world mutation')
preview.add_argument('recipe', type=Path)
preview.add_argument('--output', type=Path, required=True)
for name in ('apply', 'undo'):
p = sub.add_parser(name)
p.add_argument('--manifest', type=Path, required=True)
p.add_argument('--config', type=Path, default=ROOT / '.runtime/server/plugins/MinecraftBuilderMCP/config.yml')
p.add_argument('--console', action='store_true', help='Only for an explicitly configured isolated fixture')
p.add_argument('--execute', action='store_true', required=True, help='Explicitly perform this world edit')
if name == 'apply':
p.add_argument('recipe', type=Path)
p.add_argument('--start', type=int, required=True)
p.add_argument('--count', type=int, required=True)
args = parser.parse_args()
if args.command == 'preview':
java = Path(os.environ.get('MCB_JAVA_HOME', str(Path.home() / '.cache/minecraft-builder-mcp/jdk-25.0.2'))) / 'bin/java'
jar = ROOT / 'paper-plugin/target/paper-plugin-0.1.0-SNAPSHOT.jar'
args.output.parent.mkdir(parents=True, exist_ok=True)
subprocess.run([str(java), '-Djava.awt.headless=true', '-cp', str(jar), 'io.github.minecraftbuilder.paper.TerrainPreview', str(args.recipe.resolve()), str(args.output.resolve()), str(args.output.with_suffix('.json').resolve())], check=True)
print(args.output)
else:
run_batch(args)
if __name__ == '__main__':
main()
+222
View File
@@ -0,0 +1,222 @@
#!/usr/bin/env python3
"""Opt-in live integration checks against the isolated material-integration Paper world.
Requires .runtime/material-test-server/test-access.json provisioned by the operator.
Never targets the lobby: both scope and project identity are checked before mutation.
Run phase 'before-restart', restart the isolated server, then run 'after-restart'.
"""
import argparse
import json
import socket
import struct
import time
import urllib.error
import urllib.request
import uuid
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
RUNTIME = ROOT / '.runtime/material-test-server'
class TestServer:
def __init__(self):
self.auth = json.loads((RUNTIME / 'test-access.json').read_text())
context = self.rpc('project_context')
assert context['project_id'] == 'material-integration', 'Refusing a non-test project'
assert context['region']['max']['x'] == 511, 'Unexpected test region'
def rpc(self, method, **params):
body = json.dumps({'method': method, 'params': dict(params, player_id='console', project_id='material-integration')}).encode()
request = urllib.request.Request('http://127.0.0.1:18765/v1/rpc', data=body,
headers={'Authorization': 'Bearer ' + self.auth['agent'], 'Content-Type': 'application/json'})
try:
with urllib.request.urlopen(request, timeout=25) as response:
value = json.load(response)
except urllib.error.HTTPError as error:
value = json.load(error)
if not value['ok']:
raise RuntimeError(json.dumps(value['error']))
return value['result']
def rcon(self, command):
def receive(stream):
def exact(count):
result = b''
while len(result) < count:
piece = stream.recv(count-len(result))
if not piece:
raise EOFError('RCON disconnected')
result += piece
return result
length, = struct.unpack('<i', exact(4))
if not 10 <= length <= 1048576:
raise ValueError('Invalid RCON packet size')
packet = exact(length)
return struct.unpack('<ii', packet[:8]), packet[8:-2].decode()
def send(stream, request_id, kind, text):
packet = struct.pack('<ii', request_id, kind) + text.encode() + b'\0\0'
stream.sendall(struct.pack('<i', len(packet)) + packet)
with socket.create_connection(('127.0.0.1', 25586), timeout=10) as stream:
send(stream, 1, 3, self.auth['rcon'])
while True:
(request_id, kind), _ = receive(stream)
assert request_id != -1, 'RCON authentication failed'
if kind == 2:
break
send(stream, 2, 2, command)
while True:
(request_id, _), text = receive(stream)
if request_id == 2:
return text
def block(self, x, y, z):
pos = dict(x=x, y=y, z=z)
return self.rpc('region_inspect', min=pos, max=pos, detail='blocks')['blocks'][0]
def prepare(self, targets, expected=None):
operations = [dict(type='box', min=pos, max=pos, block=state) for pos, state in targets]
params = {'recipe': dict(version=1, operations=operations)}
if expected is not None:
params['expected_blocks'] = expected
return self.rpc('build_prepare', **params)
def apply(self, plan, expected_status='applied'):
result = self.rpc('build_apply', plan_id=plan['plan_id'], plan_hash=plan['plan_hash'], idempotency_key=str(uuid.uuid4()))
deadline = time.monotonic()+40
while result['status'] in ('queued', 'applying'):
assert time.monotonic() < deadline, 'Operation did not finish'
time.sleep(.08)
result = self.rpc('operation_status', operation_id=result['operation_id'])
assert result['status'] == expected_status, result
time.sleep(.12) # allow the terminal receipt to flush before starting another operation
return result
def undo(self, operation_id):
return self.apply(self.rpc('operation_undo_prepare', operation_id=operation_id))
def expect_error(action, code):
try:
action()
except RuntimeError as error:
assert code in str(error), str(error)
else:
raise AssertionError('Expected ' + code)
def before_restart(server):
context = server.rpc('project_context')
assert 'supported_materials' not in context
assert context['material_catalog']['blocks'] > 1000
assert len(json.dumps(context['material_catalog'])) < 256
first = server.rpc('material_search', query='trapdoor')
second = server.rpc('material_search', query='trapdoor', cursor=first['next_cursor'])
ids = [e['id'] for e in first['results']+second['results']]
assert len(first['results']) == 16 and len(ids) == len(set(ids)) == first['total']
expect_error(lambda: server.rpc('material_search', query='stairs', cursor=first['next_cursor']), 'invalid_cursor')
expect_error(lambda: server.rpc('material_search', limit=33), 'invalid_material_query')
descriptions = [server.rpc('material_describe', id=material) for material in (
'minecraft:cherry_trapdoor', 'minecraft:water', 'minecraft:oak_wall_sign',
'minecraft:redstone_wire', 'minecraft:wheat', 'minecraft:decorated_pot')]
assert set(descriptions[0]['properties']['open']) == {'true', 'false'}
assert len(descriptions[1]['properties']['level']) == 16
assert server.rpc('material_describe', id='minecraft:diamond_sword')['placeable'] is False
expect_error(lambda: server.rpc('material_describe', id='minecraft:not_a_real_material'), 'invalid_material')
server.rcon('forceload add 0 0 79 79')
server.rcon('gamerule minecraft:random_tick_speed 0')
server.rcon('fill 28 99 28 60 99 44 minecraft:stone')
# Newly available ordinary states, paired door halves, water and waterlogging.
samples = [
((32,100,32),'minecraft:cherry_trapdoor[facing=north,half=bottom,open=true,powered=false,waterlogged=true]'),
((34,100,32),'minecraft:green_carpet'),
((36,100,32),'minecraft:potted_oxeye_daisy'),
((38,100,32),'minecraft:white_candle[candles=3,lit=true,waterlogged=false]'),
((40,100,32),'minecraft:water[level=0]'),
((42,100,32),'minecraft:oak_door[facing=north,half=lower,hinge=left,open=false,powered=false]'),
((42,101,32),'minecraft:oak_door[facing=north,half=upper,hinge=left,open=false,powered=false]'),
]
targets = [(dict(zip(('x','y','z'),pos)),state) for pos,state in samples]
baseline = [server.block(*pos) for pos,_ in samples]
result = server.apply(server.prepare(targets, baseline))
for (pos, state) in targets:
assert server.block(**pos)['state'] == state
server.undo(result['operation_id'])
assert [server.block(*pos) for pos,_ in samples] == baseline
# Fixture data is known test content; private snapshots must stay out of model responses.
server.rcon('setblock 32 100 36 minecraft:chest[facing=north]')
server.rcon('data merge block 32 100 36 {Items:[{Slot:0b,id:"minecraft:diamond",count:7}]}')
server.rcon('setblock 36 100 36 minecraft:oak_sign')
server.rcon('data merge block 36 100 36 {front_text:{messages:["MCP snapshot test","","",""]}}')
server.rcon('setblock 40 100 36 minecraft:decorated_pot')
chest = server.block(32,100,36)
sign = server.block(36,100,36)
pot = server.block(40,100,36)
assert all(len(v['snapshot_id']) == 64 for v in (chest,sign,pot))
assert 'minecraft:diamond' in server.rcon('data get block 32 100 36 Items')
assert 'MCP snapshot test' in server.rcon('data get block 36 100 36 front_text')
assert 'MCP snapshot test' not in json.dumps(sign)
expect_error(lambda:server.prepare([(chest['pos'],'minecraft:stone')], [dict(pos=chest['pos'],state=chest['state'])]), 'snapshot_id')
rotate = server.apply(server.prepare([(chest['pos'],chest['state'].replace('facing=north','facing=east'))],[chest]))
assert 'minecraft:diamond' in server.rcon('data get block 32 100 36 Items')
server.undo(rotate['operation_id'])
assert server.block(32,100,36) == chest
stale = server.prepare([(chest['pos'],'minecraft:stone')],[chest])
server.rcon('data modify block 32 100 36 Items[0].count set value 9')
changed = server.block(32,100,36)
assert changed['snapshot_id'] != chest['snapshot_id']
expect_error(lambda:server.prepare([(chest['pos'],'minecraft:stone')],[chest]), 'stale_snapshot')
conflict = server.apply(stale, 'conflict')
assert 'minecraft:diamond' not in json.dumps(conflict)
assert '\u0000mcb-block' not in json.dumps(conflict)
assert conflict['conflicts'][0]['current_snapshot_id'] == changed['snapshot_id']
server.rcon('data modify block 32 100 36 Items[0].count set value 7')
assert server.block(32,100,36) == chest
# Block-entity export must fail rather than silently strip contents.
expect_error(lambda:server.rpc('schematic_export',name='reject-tile',min=chest['pos'],max=chest['pos']), 'unsupported_block_entity')
trap_pos=dict(x=44,y=100,z=36)
trap='minecraft:cherry_trapdoor[facing=north,half=bottom,open=true,powered=false,waterlogged=true]'
placed=server.apply(server.prepare([(trap_pos,trap)]))
asset=server.rpc('schematic_export',name='new-material-rotation',min=trap_pos,max=trap_pos)
target=dict(x=46,y=100,z=36)
imported=server.apply(server.rpc('schematic_import_prepare',asset_id=asset['assetId'],target=target,rotation=90))
assert 'facing=east' in server.block(**target)['state']
server.undo(imported['operation_id']);server.undo(placed['operation_id'])
before = [chest, sign, pot]
replaced = server.apply(server.prepare([(v['pos'],'minecraft:stone') for v in before],before))
item_check=server.rcon('execute if entity @e[type=minecraft:item,x=28,y=98,z=32,dx=16,dy=6,dz=8] run say UNEXPECTED_ITEM_DROP')
assert 'UNEXPECTED_ITEM_DROP' not in item_check
receipt={'catalog':context['material_catalog'],'catalog_summary_bytes':len(json.dumps(context['material_catalog'],separators=(',',':')).encode()),'search_page_bytes':len(json.dumps(first).encode()),'description_bytes':[len(json.dumps(v).encode()) for v in descriptions],
'checks':['bounded search/pagination','property domains','item-only distinction','new blocks and fluids apply/undo','paired doors','private block-entity digests','same-material inventory preservation','manual-content stale snapshot and apply conflict','no inventory drops','schematic new-material native rotation','schematic block-entity export rejection'],
'restart_operation_id':replaced['operation_id'],'before_restart_blocks':before,'phase':'awaiting_restart'}
(RUNTIME/'live-receipt.json').write_text(json.dumps(receipt,indent=2)+'\n')
server.rcon('save-all flush')
print(json.dumps({k:v for k,v in receipt.items() if k not in ('before_restart_blocks','restart_operation_id')},indent=2),flush=True)
def after_restart(server):
receipt=json.loads((RUNTIME/'live-receipt.json').read_text())
server.rcon('forceload add 0 0 79 79')
server.undo(receipt['restart_operation_id'])
for expected in receipt['before_restart_blocks']:
assert server.block(**expected['pos']) == expected
assert 'minecraft:diamond' in server.rcon('data get block 32 100 36 Items')
assert 'MCP snapshot test' in server.rcon('data get block 36 100 36 front_text')
receipt['checks'].append('complete chest/sign/pot undo after Paper restart')
receipt['phase']='complete'
(RUNTIME/'live-receipt.json').write_text(json.dumps(receipt,indent=2)+'\n')
print('Complete: chest contents, sign text and decorated-pot snapshot restored exactly after restart.',flush=True)
if __name__ == '__main__':
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('phase',choices=('before-restart','after-restart'))
args=parser.parse_args()
server=TestServer()
(before_restart if args.phase=='before-restart' else after_restart)(server)
+199
View File
@@ -0,0 +1,199 @@
"""Read-only survey completeness, cache isolation and path-height regression tests."""
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
_spec = importlib.util.spec_from_file_location('foundation_survey', Path(__file__).with_name('foundation-survey.py'))
survey = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(survey)
SCOPE = {'project_id': 'project', 'world_id': 'world', 'world_epoch': 'epoch'}
class Backend:
def __init__(self, states=None):
self.states = states or {}
self.calls = []
self.mutate = None
self.fail_call = None
self.context = {**SCOPE, 'region': {'min': {'x': -64, 'y': -64, 'z': -64},
'max': {'x': 63, 'y': 127, 'z': 63}}}
def call(self, method, **params):
self.calls.append(method)
if method == 'project_context':
return self.context
if method != 'region_inspect':
raise AssertionError('Only read-only RPC is allowed')
if self.fail_call == self.calls.count('region_inspect'):
raise RuntimeError('chunk_not_loaded')
if survey.volume(params) > 4096:
raise AssertionError('Unbounded inspection')
lo, hi = params['min'], params['max']
result = {'world_epoch': self.context['world_epoch'], 'truncated': False,
'blocks': [{'pos': {'x': x, 'y': y, 'z': z}, 'state': self.states.get((x, y, z), 'minecraft:air')}
for x in range(lo['x'], hi['x'] + 1) for y in range(lo['y'], hi['y'] + 1)
for z in range(lo['z'], hi['z'] + 1)]}
if self.mutate:
self.mutate(result)
return result
def box(lo=(-2, 64, -2), hi=(2, 68, 2)):
return survey.box_cells(dict(zip(survey.AXES, lo)), dict(zip(survey.AXES, hi)))
class SurveyTests(unittest.TestCase):
def capture(self, backend=None, cells=None):
backend = backend or Backend()
cells = cells or box()
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'survey.json.gz'
result = survey.scan(backend, cells, path)
loaded = survey.load_snapshot(path, SCOPE)
self.assertEqual(loaded.document, result.document)
return loaded
def test_negative_cell_partition_and_exact_states(self):
states = {(-2, 64, -2): 'minecraft:smooth_stone_slab[type=bottom,waterlogged=false]'}
result = self.capture(Backend(states))
self.assertEqual(result.state(-2, 64, -2), states[(-2, 64, -2)])
self.assertEqual(result.state(2, 68, 2), 'minecraft:air')
self.assertEqual(len(result.cells), 4)
with self.assertRaises(KeyError):
result.state(-3, 64, -2)
with self.assertRaises(KeyError):
result.state(17, 64, 0)
def test_columns_do_not_assume_air_in_unread_cells(self):
cells = survey.column_cells({'version': 1, 'columns': [
{'x': -20, 'z': 1, 'min_y': 64, 'max_y': 68},
{'x': 20, 'z': 1, 'min_y': 66, 'max_y': 72}]})
result = self.capture(cells=cells)
self.assertEqual(result.state(20, 69, 1), 'minecraft:air')
with self.assertRaises(KeyError):
result.state(0, 69, 1)
with self.assertRaises(KeyError):
result.state(20, 65, 1)
def test_rejects_missing_duplicate_truncated_and_wrong_epoch(self):
changes = [lambda r: r['blocks'].pop(),
lambda r: r['blocks'].append(r['blocks'][0]),
lambda r: r.update(truncated=True),
lambda r: r.update(world_epoch='different')]
for mutate in changes:
with self.subTest(mutate=mutate), tempfile.TemporaryDirectory() as directory:
backend = Backend()
backend.mutate = mutate
path = Path(directory) / 'survey.json.gz'
with self.assertRaises(RuntimeError):
survey.scan(backend, box(), path)
self.assertFalse(path.exists())
def test_out_of_area_fails_before_any_inspection(self):
backend = Backend()
with tempfile.TemporaryDirectory() as directory:
with self.assertRaises(ValueError):
survey.scan(backend, box((-65, 64, 0), (-60, 68, 0)), Path(directory) / 'survey.json.gz')
self.assertEqual(backend.calls, ['project_context'])
def test_explicit_resume_keeps_readonly_cache_and_checks_scope(self):
backend = Backend()
backend.fail_call = 2
cells = box((-16, 64, 0), (16, 68, 0))
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'survey.json.gz'
with self.assertRaisesRegex(RuntimeError, 'chunk_not_loaded'):
survey.scan(backend, cells, path)
self.assertFalse(path.exists())
with self.assertRaisesRegex(ValueError, '--resume'):
survey.scan(backend, cells, path)
backend.context['world_epoch'] = 'another-epoch'
with self.assertRaisesRegex(ValueError, 'scope'):
survey.scan(backend, cells, path, resume=True)
backend.context['world_epoch'] = 'epoch'
backend.fail_call = None
result = survey.scan(backend, cells, path, resume=True)
self.assertTrue(result.document['resumed_cache'])
self.assertEqual(backend.calls.count('region_inspect'), 4)
with self.assertRaisesRegex(ValueError, 'fresh path'):
survey.scan(backend, cells, path, resume=True)
def test_corrupt_cache_and_missing_snapshot_indices_are_rejected(self):
snapshot = self.capture()
snapshot.document['cells'][0]['indices'].pop()
with self.assertRaisesRegex(ValueError, 'indices'):
survey.Snapshot(snapshot.document, SCOPE)
def test_foreign_scope_rejected(self):
snapshot = self.capture()
with self.assertRaisesRegex(ValueError, 'another project'):
survey.Snapshot(snapshot.document, {**SCOPE, 'project_id': 'other'})
def test_walkability_half_slab_fullblock_clearance_and_gradient(self):
states = {(0, 64, 0): 'minecraft:stone',
(1, 65, 0): 'minecraft:smooth_stone_slab[type=bottom,waterlogged=false]',
(2, 65, 0): 'minecraft:stone'}
snapshot = self.capture(Backend(states), box((0, 64, 0), (3, 69, 0)))
points = [{'x': x, 'z': 0, 'standing_y': feet, 'route': 'main'}
for x, feet in [(0, 65), (1, 65.5), (2, 66)]]
result = survey.verify_walkable(snapshot, points)
self.assertTrue(result['passed'])
self.assertEqual(result['checked_route_edges'], 2)
points[1]['standing_y'] = 66
result = survey.verify_walkable(snapshot, points)
self.assertFalse(result['passed'])
self.assertIn('missing_support_at_feet', [f['reason'] for f in result['failures']])
self.assertIn('route_step_exceeds_half_block', [f['reason'] for f in result['failures']])
def test_headroom_unknown_stairs_and_absent_observations_fail(self):
states = {(0, 64, 0): 'minecraft:stone', (0, 66, 0): 'minecraft:stone',
(1, 64, 0): 'minecraft:stone_brick_stairs[facing=north,half=bottom,shape=straight,waterlogged=false]'}
snapshot = self.capture(Backend(states), box((0, 64, 0), (1, 68, 0)))
result = survey.verify_walkable(snapshot, [{'x': x, 'z': 0, 'standing_y': 65} for x in (0, 1, 2)])
self.assertEqual([f['reason'] for f in result['failures']],
['blocked_headroom', 'unknown_support_shape', 'unobserved_block'])
def test_straight_bottom_stair_profile_all_directions(self):
sides = {'north': ((.5, .75), (.5, .25)), 'south': ((.5, .25), (.5, .75)),
'east': ((.25, .5), (.75, .5)), 'west': ((.75, .5), (.25, .5))}
for facing, (low, high) in sides.items():
with self.subTest(facing=facing):
state = f'minecraft:stone_brick_stairs[facing={facing},half=bottom,shape=straight,waterlogged=false]'
snapshot = self.capture(Backend({(0, 64, 0): state}), box((0, 64, 0), (0, 68, 0)))
points = [{'x': 0, 'z': 0, 'sub_x': sub[0], 'sub_z': sub[1], 'standing_y': feet, 'route': facing}
for sub, feet in [(low, 64.5), (high, 65)]]
result = survey.verify_walkable(snapshot, points)
self.assertTrue(result['passed'], result)
self.assertEqual(result['checked_route_edges'], 1)
points[0]['standing_y'] = 65
self.assertEqual(survey.verify_walkable(snapshot, points)['failures'][0]['reason'], 'missing_support_at_feet')
def test_stair_headroom_riser_boundary_and_unsupported_shapes(self):
base = 'minecraft:stone_brick_stairs[facing=north,half=bottom,shape=straight,waterlogged=false]'
snapshot = self.capture(Backend({(0, 64, 0): base, (0, 66, 0): 'minecraft:stone'}),
box((0, 64, 0), (0, 68, 0)))
result = survey.verify_walkable(snapshot, [{'x': 0, 'z': 0, 'sub_z': .25, 'standing_y': 65}])
self.assertEqual(result['failures'][0]['reason'], 'blocked_headroom')
self.assertIsNone(survey.vertical_shape(base, .5, .5))
self.assertIsNone(survey.vertical_shape(base.replace('straight', 'inner_left'), .5, .25))
self.assertIsNone(survey.vertical_shape(base.replace('half=bottom', 'half=top'), .5, .25))
self.assertIsNone(survey.vertical_shape(base.replace('waterlogged=false', 'waterlogged=true'), .5, .25))
def test_route_subsamples_use_physical_positions_across_blocks(self):
state = 'minecraft:stone_brick_stairs[facing=north,half=bottom,shape=straight,waterlogged=false]'
snapshot = self.capture(Backend({(0, 64, 0): state, (0, 65, -1): state}),
box((0, 64, -1), (0, 70, 0)))
points = [{'x': 0, 'z': z, 'sub_z': sub_z, 'standing_y': feet, 'route': 'stair'}
for z, sub_z, feet in [(0, .75, 64.5), (0, .25, 65), (-1, .75, 65.5), (-1, .25, 66)]]
result = survey.verify_walkable(snapshot, points)
self.assertTrue(result['passed'], result)
self.assertEqual(result['checked_route_edges'], 3)
points[1]['sub_x'] = .6
result = survey.verify_walkable(snapshot, points)
self.assertIn('non_cardinal_route_step', [f['reason'] for f in result['failures']])
if __name__ == '__main__':
unittest.main()
+248
View File
@@ -0,0 +1,248 @@
"""Layout placement safety and resume checks; uses a simulated RPC world, never Minecraft."""
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
spec = importlib.util.spec_from_file_location('layout', Path(__file__).with_name('layout.py'))
layout = importlib.util.module_from_spec(spec)
spec.loader.exec_module(layout)
SCOPE = {'project_id': 'project', 'world_id': 'world', 'world_epoch': 'epoch'}
def document(blocks):
return layout.normalize({'version': 1, 'scope': SCOPE, 'blocks': blocks})
def block(x, y=60, z=0, material='minecraft:lime_concrete', expected='minecraft:air'):
return {'x': x, 'y': y, 'z': z, 'block': material, 'expected': expected}
class FakeBackend:
def __init__(self):
self.world, self.plans, self.operations, self.keys, self.calls = {}, {}, {}, {}, []
self.lose_apply = False
self.corrupt_after_apply = False
self.context = {**SCOPE, 'checked_expected_blocks': True,
'region': {'min': {'x': -512, 'y': -64, 'z': -512}, 'max': {'x': 511, 'y': 319, 'z': 511}}}
def state(self, at):
return self.world.get(at, 'minecraft:air')
def new_plan(self, changes, undo_of=None):
plan_id = 'p' + str(len(self.plans))
self.plans[plan_id] = {'changes': changes, 'undo_of': undo_of}
return {'plan_id': plan_id, 'plan_hash': 'hash-' + plan_id,
'changed_blocks': sum(e != d for _, e, d in changes)}
def call(self, method, **params):
self.calls.append((method, params))
if method == 'project_context':
return self.context
if method == 'region_inspect':
lo, hi = params['min'], params['max']
self.assert_bounded(lo, hi)
return {'world_epoch': SCOPE['world_epoch'], 'truncated': False,
'blocks': [{'pos': {'x': x, 'y': y, 'z': z}, 'state': self.state((x, y, z))}
for y in range(lo['y'], hi['y'] + 1) for z in range(lo['z'], hi['z'] + 1)
for x in range(lo['x'], hi['x'] + 1)]}
if method == 'build_prepare':
desired = {}
for op in params['recipe']['operations']:
for y in range(op['min']['y'], op['max']['y'] + 1):
for z in range(op['min']['z'], op['max']['z'] + 1):
for x in range(op['min']['x'], op['max']['x'] + 1):
desired[(x, y, z)] = op['block']
expected = {layout.position(e['pos']): e['state'] for e in params['expected_blocks']}
if desired.keys() != expected.keys():
raise AssertionError('Expected states must cover exactly the desired mask')
if any(self.state(at) != state for at, state in expected.items()):
raise RuntimeError('stale_snapshot')
return self.new_plan([(at, expected[at], state) for at, state in desired.items()])
if method == 'build_apply':
key = params['idempotency_key']
if key in self.keys:
return self.operations[self.keys[key]]
operation = 'o' + str(len(self.operations))
plan = self.plans[params['plan_id']]
written, status = 0, 'applied'
receipts = []
for at, expected, desired in plan['changes']:
if self.state(at) != expected:
status = 'conflict'
break
if expected != desired:
self.world[at] = desired
receipts.append((at, expected, desired))
written += 1
self.operations[operation] = {'operation_id': operation, 'status': status, 'written': written,
'plan_id': params['plan_id'], 'receipts': receipts}
self.keys[key] = operation
if self.corrupt_after_apply:
self.world[plan['changes'][0][0]] = 'minecraft:gold_block'
if self.lose_apply:
self.lose_apply = False
raise TimeoutError('Simulated response lost after the world changed')
return self.operations[operation]
if method == 'operation_status':
return self.operations[params['operation_id']]
if method == 'operation_undo_prepare':
source = self.operations[params['operation_id']]
changes = [(at, desired, expected) for at, expected, desired in source['receipts']]
if any(self.state(at) != expected for at, expected, _ in changes):
raise RuntimeError('Manual edit conflicts with guarded undo')
return self.new_plan(changes, params['operation_id'])
raise AssertionError(f'Unexpected RPC {method}')
@staticmethod
def assert_bounded(lo, hi):
if layout.volume({'min': lo, 'max': hi}) > 4096:
raise AssertionError('Unbounded inspection')
class LayoutTests(unittest.TestCase):
def test_dense_layout_preserves_every_position_and_respects_all_budgets(self):
value = document([block(x, y, z) for x in range(-20, 20) for y in range(63, 67) for z in range(-20, 20)])
batches = layout.make_batches(value['blocks'])
actual = [b for batch in batches for b in batch['blocks']]
self.assertEqual(sorted(actual, key=layout.position), value['blocks'])
self.assertGreater(len(batches), 1)
for batch in batches:
self.assertLessEqual(len(batch['blocks']), 4096)
self.assertLessEqual(len(batch['recipe']['operations']), 256)
self.assertLessEqual(len(layout.read_groups(batch['blocks'])), 32)
for group in layout.read_groups(batch['blocks']):
self.assertLessEqual(layout.volume(layout.bounds(group)), 4096)
self.assertEqual(layout.make_batches(list(reversed(value['blocks']))), batches)
def test_compression_does_not_fill_holes_or_merge_materials(self):
value = document([block(0), block(1), block(3), block(4, material='minecraft:red_concrete')])
operations = layout.make_batches(value['blocks'])[0]['recipe']['operations']
self.assertEqual(len(operations), 3)
lime = [o for o in operations if o['block'] == 'minecraft:lime_concrete']
self.assertEqual([(o['min']['x'], o['max']['x']) for o in lime], [(0, 1), (3, 3)])
def test_duplicate_and_invalid_coordinates_are_rejected(self):
with self.assertRaisesRegex(ValueError, 'Duplicate'):
document([block(0), block(0)])
with self.assertRaisesRegex(ValueError, '32-bit'):
document([block(True)])
def test_manual_edit_stops_before_prepare(self):
with tempfile.TemporaryDirectory() as directory:
backend = FakeBackend()
backend.world[(0, 60, 0)] = 'minecraft:gold_block'
with self.assertRaisesRegex(RuntimeError, 'Block mismatch'):
layout.apply_layout(backend, document([block(0)]), Path(directory) / 'ledger.json')
self.assertNotIn('build_prepare', [method for method, _ in backend.calls])
self.assertEqual(backend.state((0, 60, 0)), 'minecraft:gold_block')
def test_expected_surface_replacement_is_sent_atomically_and_verified(self):
with tempfile.TemporaryDirectory() as directory:
backend = FakeBackend()
backend.world[(0, 60, 0)] = 'minecraft:grass_block'
path = Path(directory) / 'ledger.json'
result = layout.apply_layout(backend, document([block(0, expected='minecraft:grass_block')]), path, progress=lambda _: None)
self.assertEqual(result['status'], 'completed')
prepared = next(params for method, params in backend.calls if method == 'build_prepare')
self.assertEqual(prepared['expected_blocks'], [{'pos': {'x': 0, 'y': 60, 'z': 0}, 'state': 'minecraft:grass_block'}])
self.assertIn('verified_at', json.loads(path.read_text())['batches'][0])
def test_lost_apply_response_resumes_same_key_without_failing_old_expected_read(self):
with tempfile.TemporaryDirectory() as directory:
backend, path = FakeBackend(), Path(directory) / 'ledger.json'
backend.lose_apply = True
value = document([block(0)])
with self.assertRaises(TimeoutError):
layout.apply_layout(backend, value, path, progress=lambda _: None)
self.assertEqual(backend.state((0, 60, 0)), 'minecraft:lime_concrete')
before = json.loads(path.read_text())['batches'][0]
self.assertIn('idempotency_key', before)
self.assertNotIn('operation_id', before)
layout.apply_layout(backend, value, path, progress=lambda _: None)
applies = [params for method, params in backend.calls if method == 'build_apply']
self.assertEqual(len(applies), 2)
self.assertEqual(applies[0], applies[1])
self.assertEqual(len(backend.plans), 1)
self.assertTrue(json.loads(path.read_text())['completed'])
def test_existing_conflict_is_not_reprepared(self):
with tempfile.TemporaryDirectory() as directory:
backend, path = FakeBackend(), Path(directory) / 'ledger.json'
value = document([block(0)])
manifest = layout.load_or_create(path, value, layout.make_batches(value['blocks']))
manifest['batches'][0]['status'] = 'conflict'
layout.terrain.save(path, manifest)
with self.assertRaisesRegex(RuntimeError, 'stopped on conflict'):
layout.apply_layout(backend, value, path)
self.assertEqual([method for method, _ in backend.calls], ['project_context'])
def test_old_server_scope_and_changed_input_are_rejected_without_write(self):
with tempfile.TemporaryDirectory() as directory:
backend, path = FakeBackend(), Path(directory) / 'ledger.json'
value = document([block(0)])
backend.context['checked_expected_blocks'] = False
with self.assertRaisesRegex(RuntimeError, 'atomic checked_expected_blocks'):
layout.apply_layout(backend, value, path)
backend.context['checked_expected_blocks'] = True
backend.context['world_epoch'] = 'other'
with self.assertRaisesRegex(RuntimeError, 'another project'):
layout.apply_layout(backend, value, path)
backend.context['world_epoch'] = 'epoch'
layout.load_or_create(path, value, layout.make_batches(value['blocks']))
with self.assertRaisesRegex(RuntimeError, 'digest'):
layout.apply_layout(backend, document([block(1)]), path)
self.assertFalse(backend.plans)
def test_post_apply_verification_stops_before_next_batch(self):
with tempfile.TemporaryDirectory() as directory:
backend, path = FakeBackend(), Path(directory) / 'ledger.json'
# More than 32 separate read cells creates several small, cheap batches.
value = document([block(x * 16, z=z * 16) for x in range(-4, 5) for z in range(-4, 5)])
backend.corrupt_after_apply = True
with self.assertRaisesRegex(RuntimeError, 'Block mismatch'):
layout.apply_layout(backend, value, path)
self.assertEqual(len(backend.plans), 1)
saved = json.loads(path.read_text())
self.assertNotIn('verified_at', saved['batches'][0])
self.assertNotIn('completed', saved)
def test_undo_is_reverse_order_and_refuses_resume_apply_after_undo(self):
with tempfile.TemporaryDirectory() as directory:
backend, path = FakeBackend(), Path(directory) / 'ledger.json'
value = document([block(x * 16, z=z * 16) for x in range(-4, 5) for z in range(-4, 5)])
layout.apply_layout(backend, value, path, progress=lambda _: None)
source_ids = [e['operation_id'] for e in json.loads(path.read_text())['batches']]
result = layout.undo_layout(backend, path, progress=lambda _: None)
self.assertEqual(result['status'], 'undone')
undo_ids = [params['operation_id'] for method, params in backend.calls if method == 'operation_undo_prepare']
self.assertEqual(undo_ids, list(reversed(source_ids)))
self.assertTrue(all(backend.state(layout.position(b)) == b['expected'] for b in value['blocks']))
with self.assertRaisesRegex(RuntimeError, 'begun undo'):
layout.apply_layout(backend, value, path)
def test_undo_does_not_start_an_unknown_apply(self):
with tempfile.TemporaryDirectory() as directory:
backend, path = FakeBackend(), Path(directory) / 'ledger.json'
backend.lose_apply = True
with self.assertRaises(TimeoutError):
layout.apply_layout(backend, document([block(0)]), path)
calls_before = len(backend.calls)
with self.assertRaisesRegex(RuntimeError, 'Uncertain apply'):
layout.undo_layout(backend, path)
self.assertEqual([m for m, _ in backend.calls[calls_before:]], ['project_context'])
def test_undo_preserves_later_manual_edits(self):
with tempfile.TemporaryDirectory() as directory:
backend, path = FakeBackend(), Path(directory) / 'ledger.json'
layout.apply_layout(backend, document([block(0)]), path, progress=lambda _: None)
backend.world[(0, 60, 0)] = 'minecraft:gold_block'
with self.assertRaisesRegex(RuntimeError, 'Manual edit'):
layout.undo_layout(backend, path)
self.assertEqual(backend.state((0, 60, 0)), 'minecraft:gold_block')
if __name__ == '__main__':
unittest.main()
+67
View File
@@ -0,0 +1,67 @@
"""Regression checks for resumable local batches; no live world needed."""
import importlib.util
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('terrain', Path(__file__).with_name('terrain.py'))
terrain = importlib.util.module_from_spec(spec)
spec.loader.exec_module(terrain)
class BatchTests(unittest.TestCase):
def test_lost_apply_response_reuses_persisted_key(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'batch.json'
entry = {'plan_id': 'plan', 'plan_hash': 'hash'}
manifest = {'tiles': [entry]}
calls = []
class Backend:
def call(self, method, **params):
calls.append((method, params))
if len(calls) == 1:
raise TimeoutError('lost response')
return {'operation_id': 'operation'}
backend = Backend()
with self.assertRaises(TimeoutError):
terrain.apply_plan(backend, entry, manifest, path)
saved = terrain.json.loads(path.read_text())
self.assertIn('idempotency_key', saved['tiles'][0])
with patch.object(terrain, 'finish', return_value={'status': 'applied', 'written': 7}):
terrain.apply_plan(backend, saved['tiles'][0], saved, path)
self.assertEqual(calls[0], calls[1])
self.assertEqual(terrain.json.loads(path.read_text())['tiles'][0]['operation_id'], 'operation')
def test_conflict_is_persisted_and_reported_not_reprepared(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'batch.json'
entry = {'plan_id': 'p', 'plan_hash': 'h', 'operation_id': 'o'}
class Backend:
def call(self, *args, **kwargs):
raise AssertionError('Must not start another apply')
with patch.object(terrain, 'finish', return_value={'status': 'conflict', 'written': 3}):
with self.assertRaisesRegex(RuntimeError, 'conflict'):
terrain.apply_plan(Backend(), entry, {'tiles': [entry]}, path)
self.assertEqual(terrain.json.loads(path.read_text())['tiles'][0]['status'], 'conflict')
def test_undo_never_starts_an_unknown_apply(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'batch.json'
scope = {'project_id': 'p', 'world_id': 'w', 'world_epoch': 'e'}
terrain.save(path, {'scope': scope, 'tiles': [{'idempotency_key': 'k', 'plan_id': 'p', 'plan_hash': 'h'}]})
class Backend:
def __init__(self, *args):
pass
def call(self, method, **params):
if method != 'project_context':
raise AssertionError('Undo must not initiate a write to discover unknown apply outcome')
return scope
args = terrain.argparse.Namespace(config=Path('unused'), console=True, manifest=path, command='undo')
with patch.object(terrain, 'Backend', Backend):
with self.assertRaisesRegex(RuntimeError, 'Uncertain apply'):
terrain.run_batch(args)
if __name__ == '__main__':
unittest.main()
+289
View File
@@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""Read-only candidate and observed-volume QA for the arrival-square balustrade.
Checks stable capped-wall states, cardinal continuity, grounded footings, original
road masks, preserved garden fixtures and remaining walkable floor. Actual mode
also compares every captured voxel against baseline plus the desired overlay.
"""
import argparse
from collections import Counter, deque
import hashlib
import importlib.util
import json
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location('balustrade_plaza_qa', ROOT / 'scripts/verify-plaza.py')
plaza = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(plaza)
survey = plaza.survey
N = {'east': (1, 0), 'north': (0, -1), 'south': (0, 1), 'west': (-1, 0)}
CAP = 'minecraft:smooth_sandstone_slab[type=bottom,waterlogged=false]'
def coordinates(document):
result = {}
for row in document['blocks']:
at = tuple(survey.point(row)[axis] for axis in ('x', 'y', 'z'))
if at in result:
raise ValueError(f'Duplicate desired position {at}')
result[at] = row['block']
return result
def stable_wall_properties(connected, capped=True):
"""Vanilla 26.2 WallBlock under a full bottom face (the bottom slab cap).
Cap coverage makes arms tall. shouldRaisePost suppresses the post for either
opposite tall pair only after its endpoint/corner/T asymmetry condition.
"""
ns = connected['north'] != connected['south']
ew = connected['east'] != connected['west']
opposite = ((connected['north'] and connected['south']) or
(connected['east'] and connected['west']))
up = ns or ew or not opposite
return {**{name: ('tall' if capped else 'low') if connected[name] else 'none' for name in N},
'up': str(up).lower(), 'waterlogged': 'false'}
def wall_neighbor(state):
name, _ = plaza.parse(state)
return name.endswith('_wall') or name in plaza.STATIC_CUBES
def check_guard(reader, metadata):
columns = set(map(tuple, metadata['fence_columns']))
piers = set(map(tuple, metadata['pier_columns']))
failures, listed = [], []
if not columns or not piers <= columns:
raise ValueError('Fence columns and piers must form a nonempty valid set')
for index, raw in enumerate(metadata['path_runs']):
run = list(map(tuple, raw))
if len(run) < 2 or run[0] not in piers or run[-1] not in piers:
failures.append({'reason': 'run_needs_pier_endpoints', 'run': index})
for a, b in zip(run, run[1:]):
if abs(a[0] - b[0]) + abs(a[1] - b[1]) != 1:
failures.append({'reason': 'noncardinal_guard_gap', 'from': a, 'to': b})
listed.extend(run)
if len(listed) != len(set(listed)) or set(listed) != columns:
failures.append({'reason': 'path_runs_do_not_cover_fence_once'})
for x, z in sorted(columns):
state = reader.state(x, 96, z)
material, props = plaza.parse(state)
if (x, z) in piers:
if material != 'cut_sandstone':
failures.append({'at': [x, 96, z], 'reason': 'pier_material', 'actual': state})
else:
connections = {name: wall_neighbor(reader.state(x + dx, 96, z + dz))
for name, (dx, dz) in N.items()}
expected = stable_wall_properties(connections)
if material != 'stone_brick_wall' or props != expected:
failures.append({'at': [x, 96, z], 'reason': 'unstable_wall_state',
'actual': state, 'expected_properties': expected})
if reader.state(x, 97, z) != CAP:
failures.append({'at': [x, 97, z], 'reason': 'missing_continuous_bottom_cap'})
if plaza.parse(reader.state(x, 95, z))[0] not in plaza.STATIC_CUBES:
failures.append({'at': [x, 95, z], 'reason': 'guard_without_full_base'})
for footing in metadata['footing_columns']:
x, z, bottom = footing['x'], footing['z'], footing['support_y']
if (x, z) not in columns or not 70 <= bottom < 95:
raise ValueError('Unexpected footing coordinate or support height')
if plaza.parse(reader.state(x, bottom, z))[0] == 'grass_block':
failures.append({'at': [x, bottom, z], 'reason': 'grass_under_opaque_footing_will_decay_to_dirt'})
for y in range(bottom, 96):
if plaza.parse(reader.state(x, y, z))[0] not in plaza.STATIC_CUBES:
failures.append({'at': [x, y, z], 'reason': 'footing_gap_or_unknown_support'})
# A newly connected pier also changes the reciprocal side of an old road
# rail. Inspect real neighbors, including walls absent from compiler masks.
adjacent = {(x + dx, z + dz) for x, z in columns for dx, dz in N.values()
if (x + dx, z + dz) not in columns and
plaza.parse(reader.state(x + dx, 96, z + dz))[0] == 'stone_brick_wall'}
for x, z in sorted(adjacent):
above = reader.state(x, 97, z)
if above not in survey.AIR and above != CAP:
failures.append({'at': [x, 97, z], 'reason': 'unknown_neighbor_rail_top_shape', 'actual': above})
continue
connected = {name: wall_neighbor(reader.state(x + dx, 96, z + dz))
for name, (dx, dz) in N.items()}
actual = reader.state(x, 96, z)
expected = stable_wall_properties(connected, above == CAP)
if plaza.parse(actual)[1] != expected:
failures.append({'at': [x, 96, z], 'reason': 'unstable_reciprocal_road_rail',
'actual': actual, 'expected_properties': expected})
pending, components = set(columns), []
while pending:
seen, queue = set(), deque([next(iter(pending))])
while queue:
p = queue.popleft()
if p not in pending:
continue
pending.remove(p)
seen.add(p)
queue.extend((p[0] + dx, p[1] + dz) for dx, dz in N.values())
components.append(len(seen))
if sorted(components) != sorted(len(run) for run in metadata['path_runs']):
failures.append({'reason': 'actual_cardinal_components_differ_from_runs', 'components': components})
return {'passed': not failures, 'columns': len(columns), 'piers': len(piers),
'cardinal_components': sorted(components), 'grounded_footings': len(metadata['footing_columns']),
'adjacent_road_rails': len(adjacent),
'failures': failures}
def captured_voxels(snapshot):
for cell in snapshot.cells.values():
lo, hi, index = cell['min'], cell['max'], 0
for y in range(lo['y'], hi['y'] + 1):
for z in range(lo['z'], hi['z'] + 1):
for x in range(lo['x'], hi['x'] + 1):
yield (x, y, z), cell['palette'][cell['indices'][index]]
index += 1
def compare_volume(before, after, desired):
mismatches, counts = [], Counter()
for at, baseline in captured_voxels(before):
changed = at in desired
counts['desired_voxels' if changed else 'outside_desired_voxels'] += 1
actual, expected = after.state(*at), desired.get(at, baseline)
if actual != expected:
counts['desired_mismatches' if changed else 'outside_desired_mismatches'] += 1
if len(mismatches) < 30:
mismatches.append({'at': list(at), 'expected': expected, 'actual': actual,
'inside_desired': changed})
if counts['desired_voxels'] != len(desired):
raise ValueError('Desired blocks extend outside the captured baseline volume')
return {'passed': not mismatches, **dict(counts), 'mismatch_examples': mismatches,
'scope_note': 'Every voxel inside the supplied before snapshot; no claim for unobserved exterior voxels.'}
def audit(before, layout, metadata, garden_plan, garden_metadata, garden_walk, nav, after=None):
scope = survey.scope_of(layout['scope'])
if any(value != scope for value in (before.scope, metadata['scope'], garden_plan['scope'],
garden_metadata['scope'], garden_walk['scope'])) or (after and after.scope != scope):
raise ValueError('Project/world/epoch differs between inputs')
desired, failures = coordinates(layout), []
for row in layout['blocks']:
at = tuple(row[a] for a in ('x', 'y', 'z'))
if before.state(*at) != row['expected']:
raise ValueError(f'Stale expected block at {at}')
fence = set(map(tuple, metadata['fence_columns']))
road = {tuple(p) for route in nav['routes'] for p in route['clear_cells']}
bench = set(map(tuple, garden_metadata['bench_access_columns']))
for x, y, z in desired:
if (x, z) in road or (x, z) in bench:
failures.append({'at': [x, y, z], 'reason': 'protected_road_or_bench_column_edited'})
if fence & (road | bench):
failures.append({'reason': 'fence_declared_on_protected_road_or_bench'})
reader = plaza.Reader(after or before, None if after else desired)
guard = check_guard(reader, metadata)
old_desired = coordinates(garden_plan)
protected = {at: state for at, state in old_desired.items()
if plaza.parse(state)[0] in plaza.FLOWERS | {'spruce_stairs', 'spruce_fence', 'lantern',
'iron_chain', 'iron_bars', 'gold_block'} or any(part in state for part in ('_leaves', '_log', 'waxed_oxidized_cut_copper'))}
for fixture in garden_metadata['fixtures']:
if fixture['type'] in ('conifer', 'topiary'):
at = fixture['x'], 95, fixture['z']
protected[at] = before.state(*at)
lost = [{'at': list(at), 'expected': state, 'actual': reader.state(*at)}
for at, state in protected.items() if reader.state(*at) != state]
fixture_report = plaza.check_fixtures(reader, old_desired, garden_metadata)
blocked = set(map(tuple, garden_metadata['unwalkable_columns'])) | fence
points = [p for p in garden_walk['points'] if (p['x'], p['z']) not in fence]
walking = survey.verify_walkable(reader, points)
navigation = json.loads(json.dumps(nav))
for cell in navigation['cells']:
if (cell['x'], cell['z']) in blocked:
cell['clear'] = False
nav_report = plaza.navigation.audit(navigation)
graph = plaza.navigation.surface_graph(plaza.navigation.normalize_cells(navigation))
reached = plaza.navigation.reachable(graph, (0, 9))
bench_report = [plaza.navigation.endpoint_report(f'bench-access-{x}-{z}', (x, z), graph, reached)
for x, z in sorted(bench)]
nav_report['bench_access'] = bench_report
nav_report['passed'] &= all(row['passed'] for row in bench_report)
volume = compare_volume(before, after, desired) if after else None
passed = (not failures and not lost and guard['passed'] and fixture_report['passed'] and
walking['passed'] and nav_report['passed'] and (volume is None or volume['passed']))
return {'version': 1, 'scope': scope, 'mode': 'actual_after_snapshot' if after else 'candidate_overlay',
'passed': passed, 'world_edits': 0, 'desired_blocks': len(desired), 'guard': guard,
'protected_road_columns': len(road), 'protection_failures': failures,
'preserved_fixture_states': len(protected), 'lost_fixtures': lost[:30],
'fixtures': fixture_report, 'walking': walking, 'navigation': nav_report,
'volume': volume, 'note': 'Point-sampled body headroom and navigation; not a full moving-player collision simulation.'}
class BalustradeTests(unittest.TestCase):
def test_stable_capped_wall_corner_and_straight_and_cross(self):
for sides, up in [({'north', 'south'}, 'false'), ({'east', 'west'}, 'false'),
(set(N), 'false'), ({'north', 'east'}, 'true'),
({'north', 'east', 'south'}, 'true'), ({'north'}, 'true')]:
self.assertEqual(stable_wall_properties({n: n in sides for n in N})['up'], up)
uncapped = stable_wall_properties({n: n in {'east', 'west'} for n in N}, False)
self.assertEqual((uncapped['east'], uncapped['west'], uncapped['up']), ('low', 'low', 'false'))
def test_volume_detects_unrelated_change_and_exact_properties(self):
before = type('Snapshot', (), {'cells': {0: {'min': dict(x=0, y=0, z=0),
'max': dict(x=1, y=0, z=0), 'palette': ['minecraft:air'], 'indices': [0, 0]}}})()
states = {(0, 0, 0): CAP, (1, 0, 0): 'minecraft:stone'}
after = type('After', (), {'state': lambda self, x, y, z: states[x, y, z]})()
result = compare_volume(before, after, {(0, 0, 0): CAP})
self.assertFalse(result['passed'])
self.assertEqual(result['outside_desired_mismatches'], 1)
states[(1, 0, 0)] = 'minecraft:air'
self.assertTrue(compare_volume(before, after, {(0, 0, 0): CAP})['passed'])
states[(0, 0, 0)] = CAP.replace('bottom', 'top')
self.assertEqual(compare_volume(before, after, {(0, 0, 0): CAP})['desired_mismatches'], 1)
def test_diagonal_run_is_rejected(self):
states = {(x, y, z): 'minecraft:air' for x in range(-1, 3) for z in range(-1, 3) for y in range(95, 98)}
for x, z in [(0, 0), (1, 1)]:
states[x, 95, z] = 'minecraft:stone'
states[x, 96, z] = 'minecraft:cut_sandstone'
states[x, 97, z] = CAP
reader = type('Reader', (), {'state': lambda self, x, y, z: states[x, y, z]})()
meta = {'fence_columns': [[0, 0], [1, 1]], 'pier_columns': [[0, 0], [1, 1]],
'path_runs': [[[0, 0], [1, 1]]], 'footing_columns': []}
result = check_guard(reader, meta)
self.assertIn('noncardinal_guard_gap', {f['reason'] for f in result['failures']})
def main():
parser = argparse.ArgumentParser(description=__doc__)
stage, garden = ROOT / '.runtime/balustrade-stage04', ROOT / '.runtime/plaza-stage03'
parser.add_argument('--before', type=Path, default=stage / 'before-full.json.gz')
parser.add_argument('--after', type=Path)
parser.add_argument('--layout', type=Path, default=stage / 'balustrade.json')
parser.add_argument('--metadata', type=Path, default=stage / 'balustrade.metadata.json')
parser.add_argument('--garden-plan', type=Path, default=garden / 'plaza-polished.json')
parser.add_argument('--garden-metadata', type=Path, default=garden / 'plaza-polished.metadata.json')
parser.add_argument('--garden-walk', type=Path, default=garden / 'plaza-polished.walk.json')
parser.add_argument('--navigation', type=Path, default=ROOT / '.runtime/foundations-stage02/navigation-verified-walkable-input.json')
parser.add_argument('--report', type=Path, default=stage / 'candidate-qa.json')
parser.add_argument('--self-test', action='store_true')
args = parser.parse_args()
if args.self_test:
result = unittest.TextTestRunner().run(unittest.defaultTestLoader.loadTestsFromTestCase(BalustradeTests))
raise SystemExit(0 if result.wasSuccessful() else 1)
paths = {key: getattr(args, key) for key in ('layout', 'metadata', 'garden_plan', 'garden_metadata', 'garden_walk', 'navigation')}
documents = [json.loads(path.read_text()) for path in paths.values()]
before = survey.load_snapshot(args.before)
after = survey.load_snapshot(args.after, before.scope) if args.after else None
report = audit(before, *documents, after=after)
paths['before'] = args.before
if args.after:
paths['after'] = args.after
report['input_sha256'] = {key: hashlib.sha256(path.read_bytes()).hexdigest() for key, path in paths.items()}
args.report.parent.mkdir(parents=True, exist_ok=True)
survey.terrain.save(args.report, report)
print(json.dumps({key: report[key] for key in ('passed', 'mode', 'desired_blocks', 'guard', 'protection_failures',
'lost_fixtures', 'fixtures', 'volume')}
| {'walk_points': report['walking']['checked_points'], 'walk_failures': report['walking']['failures'][:10],
'navigation_passed': report['navigation']['passed'],
'unreachable_columns': report['navigation']['unreachable_clear_columns'],
'report': str(args.report.resolve())}, indent=2))
raise SystemExit(0 if report['passed'] else 1)
if __name__ == '__main__':
main()
+423
View File
@@ -0,0 +1,423 @@
#!/usr/bin/env python3
"""Verify a foundation edit against actual before/after Paper surface maps.
Air cuts expose saved lower voxel states; an omitted voxel never becomes assumed
air. The whole map is compared, including columns outside the edit. Optional PNG
output is a fresh material/height render of the observed after map, not a mockup.
"""
import argparse
from collections import Counter, defaultdict
import hashlib
import importlib.util
import json
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
_spec = importlib.util.spec_from_file_location('foundation_snapshot', ROOT / 'scripts/foundation-survey.py')
survey = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(survey)
AIR = {'minecraft:air', 'minecraft:cave_air', 'minecraft:void_air'}
WATER = {'minecraft:water', 'minecraft:bubble_column'}
MAP_SCOPE = ('world_uuid', 'world_key', 'min_x', 'max_x', 'min_z', 'max_z', 'width', 'length')
def material(state):
return state.split('[', 1)[0]
def ignored_materials(document):
"""Air stays implicit; absent metadata preserves legacy map semantics."""
raw = document.get('ignored_materials', [])
if (not isinstance(raw, list) or any(not isinstance(value, str) for value in raw)
or len(raw) != len(set(raw))
or not set(raw) <= AIR | {'minecraft:barrier'}):
raise ValueError('Unsupported explicit ignored_materials map policy')
return AIR | set(raw)
def validate_map(document):
if document.get('format') != 'minecraft-builder-surface-map-v1' or document.get('source') != 'paper_world_surface':
raise ValueError('Expected a captured Paper world surface map')
for key in ('min_x', 'max_x', 'min_z', 'max_z', 'width', 'length'):
if type(document.get(key)) is not int:
raise ValueError('Map bounds must be integers')
count = document['width'] * document['length']
if (not 0 < count <= 4_194_304 or document['width'] != document['max_x'] - document['min_x'] + 1
or document['length'] != document['max_z'] - document['min_z'] + 1):
raise ValueError('Map bounds and dimensions disagree')
palette = document.get('palette')
if not isinstance(palette, list) or not palette or any(not isinstance(m, str) or not survey.STATE.fullmatch(m) or '[' in m for m in palette):
raise ValueError('Map palette must contain material names')
heights, indices = document.get('surface_y'), document.get('material_index')
if not isinstance(heights, list) or len(heights) != count or any(type(y) is not int for y in heights):
raise ValueError('Map has missing or invalid surface heights')
if not isinstance(indices, list) or len(indices) != count or any(type(i) is not int or not 0 <= i < len(palette) for i in indices):
raise ValueError('Map has missing or invalid material indices')
ignored = ignored_materials(document)
if any(palette[index] in ignored - AIR for index in indices):
raise ValueError('Map surface contains a material its explicit policy ignores')
return [palette[index] for index in indices]
def column_index(document, x, z):
if not (document['min_x'] <= x <= document['max_x'] and document['min_z'] <= z <= document['max_z']):
raise ValueError(f'Edited column {(x, z)} is outside the surface map')
return (z - document['min_z']) * document['width'] + x - document['min_x']
def expected_surface(before, snapshot, layout):
original_materials = validate_map(before)
ignored = ignored_materials(before)
scope = survey.scope_of(layout.get('scope', {}))
if snapshot.scope != scope or scope['world_id'] != before['world_uuid']:
raise ValueError('Voxel snapshot, layout and map scopes differ')
overlays = defaultdict(dict)
actual_changes = Counter()
water_replacements = []
expected_states_checked = 0
blocks = layout.get('blocks')
if layout.get('version') != 1 or not isinstance(blocks, list) or not blocks:
raise ValueError('Layout requires version 1 and explicit blocks')
for block in blocks:
pos = survey.point(block)
x, y, z = pos['x'], pos['y'], pos['z']
column_index(before, x, z)
if y in overlays[(x, z)]:
raise ValueError(f'Duplicate desired block {(x, y, z)}')
desired, expected = block.get('block'), block.get('expected')
if any(not isinstance(s, str) or not survey.STATE.fullmatch(s) for s in (desired, expected)):
raise ValueError('Layout requires valid desired and expected block states')
observed = snapshot.state(x, y, z)
if observed != expected:
raise ValueError(f'Layout expected state disagrees with baseline voxel snapshot at {(x, y, z)}')
expected_states_checked += 1
overlays[(x, z)][y] = desired
if desired != observed:
actual_changes['removed' if material(desired) in AIR else 'placed_or_replaced'] += 1
if material(observed) in WATER and desired != observed:
water_replacements.append({'x': x, 'y': y, 'z': z, 'before': observed, 'desired': desired})
heights = before['surface_y'].copy()
materials = original_materials.copy()
exposed_snapshot_blocks = 0
map_only_covered_tops = 0
for (x, z), overlay in overlays.items():
i = column_index(before, x, z)
original_top = before['surface_y'][i]
# A map and a block capture taken at different times must still agree
# where reconstruction relies on their shared original surface.
visible_overlay_top = max((y for y, state in overlay.items() if material(state) not in ignored),
default=original_top)
try:
observed_original_top = material(snapshot.state(x, original_top, z))
except KeyError:
# An overhead addition can be surveyed independently of the ground
# it covers. Its checked desired voxel proves the new surface lies
# above the earlier captured top; no lower voxel is reconstructed.
# Cuts and invisible-only additions still require that observation.
if visible_overlay_top <= original_top:
raise
observed_original_top = original_materials[i]
map_only_covered_tops += 1
empty_fallback = original_materials[i] in AIR
if (observed_original_top != original_materials[i]
and not (empty_fallback and observed_original_top in ignored)):
raise ValueError(f'Before map and voxel snapshot disagree at original surface {(x, original_top, z)}')
top = max(original_top, visible_overlay_top)
while True:
if top in overlay:
state = overlay[top]
elif top == original_top:
state = original_materials[i]
elif top > original_top:
# The captured surface proves higher untouched cells are
# ignored by this explicit visibility policy. They may be
# invisible barriers; this is no claim of collision-free air.
state = 'minecraft:air'
else:
state = snapshot.state(x, top, z)
exposed_snapshot_blocks += 1
if material(state) not in ignored:
heights[i], materials[i] = top, material(state)
break
if empty_fallback and top == original_top:
# A captured all-transparent column reports air at world min Y.
# Do not read below that explicit fallback or invent terrain.
heights[i], materials[i] = top, original_materials[i]
break
top -= 1
return heights, materials, overlays, {
'expected_states_checked_against_baseline': expected_states_checked,
'placed_or_replaced_voxels': actual_changes['placed_or_replaced'], 'removed_voxels': actual_changes['removed'],
'lower_snapshot_voxels_consulted_after_cuts': exposed_snapshot_blocks,
'covered_original_tops_known_only_from_before_map': map_only_covered_tops,
'observed_water_voxels_replaced': water_replacements}
def verify(before, after, snapshot, layout):
after_materials = validate_map(after)
if ignored_materials(before) != ignored_materials(after):
raise ValueError('Before and after map ignored_materials policies differ; capture a matching baseline')
for key in MAP_SCOPE:
if before.get(key) != after.get(key):
raise ValueError(f'Before and after map scopes differ: {key}')
expected_y, expected_m, overlays, stats = expected_surface(before, snapshot, layout)
original_materials = [before['palette'][i] for i in before['material_index']]
affected = {column_index(before, x, z) for x, z in overlays}
mismatches, outside_mismatches = [], 0
changed_surface = 0
water_columns = 0
water_surface_changes = []
for i, observed in enumerate(after_materials):
x, z = i % before['width'] + before['min_x'], i // before['width'] + before['min_z']
if before['surface_y'][i] != after['surface_y'][i] or original_materials[i] != observed:
changed_surface += 1
if expected_y[i] != after['surface_y'][i] or expected_m[i] != observed:
mismatch = {'x': x, 'z': z, 'expected_y': expected_y[i], 'actual_y': after['surface_y'][i],
'expected_material': expected_m[i], 'actual_material': observed, 'inside_edit_columns': i in affected}
mismatches.append(mismatch)
if i not in affected:
outside_mismatches += 1
if original_materials[i] in WATER:
water_columns += 1
if observed != original_materials[i] or before['surface_y'][i] != after['surface_y'][i]:
water_surface_changes.append({'x': x, 'z': z, 'before_y': before['surface_y'][i],
'after_y': after['surface_y'][i], 'after_material': observed})
water_ok = not water_surface_changes and not stats['observed_water_voxels_replaced']
return {'version': 1, 'world': before['world'], 'scope': snapshot.scope,
'source': 'Full live Paper surface maps plus observed baseline voxels and desired edit overlay',
'surface_ignored_materials': sorted(ignored_materials(after)),
'verified_surface_columns': len(expected_y), 'affected_columns': len(affected),
'outside_edit_columns_verified': len(expected_y) - len(affected),
'changed_surface_columns': changed_surface, 'surface_mismatches': len(mismatches),
'outside_edit_surface_mismatches': outside_mismatches,
'mismatch_examples': mismatches[:40],
'original_visible_water_columns': water_columns,
'visible_water_surface_changes': len(water_surface_changes),
'water_surface_change_examples': water_surface_changes[:20],
**stats, 'water_preservation_passed': water_ok,
'passed': not mismatches and water_ok,
'capture_started_at': after.get('capture_started_at'), 'capture_finished_at': after.get('capture_finished_at'),
'atomic_snapshot': False,
'note': 'Sequential map captures; edits must be idle during each capture. This checks every visible surface and the observed water blocks in the edit. Hidden untouched blocks outside the voxel survey are not inferred. Live voxel and headroom checks are separate.'}
def render(after, layout, report, path):
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.colors import to_rgb
heights = np.asarray(after['surface_y'], dtype=float).reshape(after['length'], after['width'])
ids = np.asarray(after['material_index']).reshape(heights.shape)
colors = after.get('palette_rgb')
if not isinstance(colors, list) or len(colors) != len(after['palette']):
raise ValueError('PNG rendering requires the actual map palette_rgb array')
rgb = np.asarray([to_rgb(c) for c in colors])[ids]
dz, dx = np.gradient(heights)
light = (.55 * dx + .55 * dz + .63) / np.sqrt(dx * dx + dz * dz + 1)
shade = np.clip(.76 + .36 * light, .44, 1.13)
for index, name in enumerate(after['palette']):
if name.endswith(('_concrete', '_wool', '_terracotta')):
shade[ids == index] = 1
if name in WATER:
shade[ids == index] = .96
rgb = np.clip(rgb * shade[:, :, None], 0, 1)
blocks = layout['blocks']
x0, x1 = min(b['x'] for b in blocks) - 20, max(b['x'] for b in blocks) + 20
z0, z1 = min(b['z'] for b in blocks) - 20, max(b['z'] for b in blocks) + 20
fig, ax = plt.subplots(figsize=(11, 14), facecolor='#f1eee4')
ax.set_facecolor('#f1eee4')
ax.imshow(rgb, origin='upper', interpolation='nearest',
extent=(after['min_x'], after['max_x'] + 1, after['max_z'] + 1, after['min_z']))
ax.set(xlim=(x0, x1), ylim=(z1, z0), xlabel='X · east →', ylabel='Z (positive south)')
ax.set_aspect('equal')
ax.tick_params(colors='#4f594d', labelsize=9)
for spine in ax.spines.values():
spine.set_color('#9a9e8c')
labels = [
((0, 9), (64, -10), '01 ARRIVAL SQUARE'),
((-14, -117), (33, -156), '02 CLOCK STATION'),
((-6, -79), (54, -75), 'Station steps'),
((-7, -61), (37, -44), 'Forecourt'),
((1, 91), (-57, 115), 'South approach'),
((-75, 30), (-74, 62), 'Lake approach'),
((-60, -36), (-98, -23), 'Portal approach'),
((-63, -72), (-93, -103), 'Northwest promenade'),
((67, -84), (77, -117), 'Northeast promenade'),
((68, 10), (72, 42), 'East approach')]
for (x, z), (tx, tz), label in labels:
ax.annotate(label, xy=(x + .5, z + .5), xytext=(tx, tz),
ha='center', va='center', fontsize=8.7 if label[:2] in ('01', '02') else 8,
color='#263b31', weight='bold' if label[:2] in ('01', '02') else 'normal',
bbox=dict(boxstyle='round,pad=.3', fc='#f8f5ea', ec='#849080', lw=.6, alpha=.96),
arrowprops=dict(arrowstyle='-', color='#354b3c', lw=.8, shrinkA=3, shrinkB=2))
ax.annotate('N', xy=(x1 - 8, z0 + 4), xytext=(x1 - 8, z0 + 18), ha='center', va='center',
color='#20392d', fontsize=11, weight='bold', arrowprops=dict(arrowstyle='-|>', color='#20392d'))
bar_x, bar_z = x0 + 8, z1 - 8
ax.plot([bar_x, bar_x + 25], [bar_z, bar_z], color='#20392d', lw=2)
ax.text(bar_x + 12.5, bar_z - 2, '25 blocks', ha='center', va='bottom', fontsize=8, color='#20392d',
bbox=dict(fc='#f8f5ea', ec='none', alpha=.8, pad=1))
fig.suptitle('SHACRAFT / FOUNDATIONS 01 + 02', x=.5, y=.975, fontsize=17, weight='bold', color='#20392d')
fig.text(.5, .945, 'Actual server surface · north up · local roads and supported foundations',
ha='center', fontsize=10, color='#52614e')
status = 'surface check passed' if report['passed'] else f"{report['surface_mismatches']} surface mismatches"
fig.text(.5, .025, f"{report['verified_surface_columns']:,} map columns checked · {status}\n"
'Material colors and relief come from the captured world. Labels are annotations.',
ha='center', fontsize=9, color='#52614e', linespacing=1.6)
fig.subplots_adjust(left=.09, right=.96, top=.923, bottom=.077)
path.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(path, dpi=180, facecolor=fig.get_facecolor())
plt.close(fig)
class FakeSnapshot:
scope = {'project_id': 'project', 'world_id': 'world', 'world_epoch': 'epoch'}
def __init__(self, states):
self.states = states
def state(self, x, y, z):
return self.states[(x, y, z)]
def fixture_map(heights, materials):
palette = list(dict.fromkeys(materials))
return {'format': 'minecraft-builder-surface-map-v1', 'source': 'paper_world_surface',
'world': 'test', 'world_uuid': 'world', 'world_key': 'minecraft:test',
'min_x': 0, 'max_x': len(heights) - 1, 'min_z': 0, 'max_z': 0,
'width': len(heights), 'length': 1, 'surface_y': heights,
'palette': palette, 'material_index': [palette.index(m) for m in materials]}
class FoundationMapTests(unittest.TestCase):
def baseline(self):
before = fixture_map([10, 12, 8], ['minecraft:grass_block', 'minecraft:gold_block', 'minecraft:water'])
snapshot = FakeSnapshot({(0, 10, 0): 'minecraft:grass_block', (0, 9, 0): 'minecraft:air',
(0, 8, 0): 'minecraft:stone'})
layout = {'version': 1, 'scope': snapshot.scope,
'blocks': [{'x': 0, 'y': 10, 'z': 0, 'block': 'minecraft:air', 'expected': 'minecraft:grass_block'}]}
return before, snapshot, layout
def test_cut_exposes_lower_observed_block_and_preserves_other_columns(self):
before, snapshot, layout = self.baseline()
after = fixture_map([8, 12, 8], ['minecraft:stone', 'minecraft:gold_block', 'minecraft:water'])
report = verify(before, after, snapshot, layout)
self.assertTrue(report['passed'])
self.assertEqual(report['lower_snapshot_voxels_consulted_after_cuts'], 2)
self.assertEqual(report['outside_edit_columns_verified'], 2)
self.assertEqual(report['original_visible_water_columns'], 1)
def test_unrelated_column_change_is_detected(self):
before, snapshot, layout = self.baseline()
after = fixture_map([8, 11, 8], ['minecraft:stone', 'minecraft:gold_block', 'minecraft:water'])
report = verify(before, after, snapshot, layout)
self.assertFalse(report['passed'])
self.assertEqual(report['outside_edit_surface_mismatches'], 1)
self.assertEqual(report['mismatch_examples'][0]['x'], 1)
def test_missing_lower_voxel_is_not_treated_as_air(self):
before, snapshot, layout = self.baseline()
del snapshot.states[(0, 9, 0)]
with self.assertRaises(KeyError):
expected_surface(before, snapshot, layout)
def test_new_surface_block_properties_reduce_to_material(self):
before, snapshot, layout = self.baseline()
snapshot.states[(0, 11, 0)] = 'minecraft:air'
layout['blocks'].append({'x': 0, 'y': 11, 'z': 0, 'expected': 'minecraft:air',
'block': 'minecraft:smooth_stone_slab[type=bottom,waterlogged=false]'})
after = fixture_map([11, 12, 8], ['minecraft:smooth_stone_slab', 'minecraft:gold_block', 'minecraft:water'])
self.assertTrue(verify(before, after, snapshot, layout)['passed'])
def test_stale_expected_state_and_surface_source_mismatch_rejected(self):
before, snapshot, layout = self.baseline()
layout['blocks'][0]['expected'] = 'minecraft:dirt'
with self.assertRaisesRegex(ValueError, 'baseline'):
expected_surface(before, snapshot, layout)
layout['blocks'][0]['expected'] = 'minecraft:grass_block'
before['palette'][0] = 'minecraft:dirt'
with self.assertRaisesRegex(ValueError, 'original surface'):
expected_surface(before, snapshot, layout)
def test_explicit_barrier_roof_is_invisible_but_legacy_roof_is_visible(self):
before, snapshot, _ = self.baseline()
snapshot.states[(0, 20, 0)] = 'minecraft:air'
layout = {'version': 1, 'scope': snapshot.scope, 'blocks': [
{'x': 0, 'y': 20, 'z': 0, 'block': 'minecraft:barrier', 'expected': 'minecraft:air'}]}
legacy_after = fixture_map([20, 12, 8], ['minecraft:barrier', 'minecraft:gold_block', 'minecraft:water'])
self.assertTrue(verify(before, legacy_after, snapshot, layout)['passed'])
visible_after = json.loads(json.dumps(before))
before['ignored_materials'] = visible_after['ignored_materials'] = ['minecraft:barrier']
self.assertTrue(verify(before, visible_after, snapshot, layout)['passed'])
def test_replacing_visible_top_with_ignored_barrier_exposes_lower_observed_stone(self):
before, snapshot, layout = self.baseline()
layout['blocks'][0]['block'] = 'minecraft:barrier'
after = fixture_map([8, 12, 8], ['minecraft:stone', 'minecraft:gold_block', 'minecraft:water'])
before['ignored_materials'] = after['ignored_materials'] = sorted(AIR | {'minecraft:barrier'})
self.assertTrue(verify(before, after, snapshot, layout)['passed'])
def test_policy_change_is_not_silently_compared_to_legacy_map(self):
before, snapshot, layout = self.baseline()
after = fixture_map([8, 12, 8], ['minecraft:stone', 'minecraft:gold_block', 'minecraft:water'])
after['ignored_materials'] = sorted(AIR | {'minecraft:barrier'})
with self.assertRaisesRegex(ValueError, 'policies differ'):
verify(before, after, snapshot, layout)
def test_all_transparent_column_keeps_world_min_air_fallback(self):
before = fixture_map([-64], ['minecraft:air'])
before['ignored_materials'] = ['minecraft:barrier']
after = json.loads(json.dumps(before))
snapshot = FakeSnapshot({(0, -64, 0): 'minecraft:barrier', (0, 20, 0): 'minecraft:air'})
layout = {'version': 1, 'scope': snapshot.scope, 'blocks': [
{'x': 0, 'y': 20, 'z': 0, 'block': 'minecraft:barrier', 'expected': 'minecraft:air'}]}
self.assertTrue(verify(before, after, snapshot, layout)['passed'])
def test_overhead_addition_uses_captured_map_below_tight_voxel_survey(self):
before = fixture_map([10], ['minecraft:grass_block'])
snapshot = FakeSnapshot({(0, 20, 0): 'minecraft:air'})
layout = {'version': 1, 'scope': snapshot.scope, 'blocks': [
{'x': 0, 'y': 20, 'z': 0, 'block': 'minecraft:stone', 'expected': 'minecraft:air'}]}
after = fixture_map([20], ['minecraft:stone'])
report = verify(before, after, snapshot, layout)
self.assertTrue(report['passed'])
self.assertEqual(report['covered_original_tops_known_only_from_before_map'], 1)
before['ignored_materials'] = ['minecraft:barrier']
layout['blocks'][0]['block'] = 'minecraft:barrier'
with self.assertRaises(KeyError):
expected_surface(before, snapshot, layout)
def main():
parser = argparse.ArgumentParser(description=__doc__)
maps = ROOT / '.runtime/server/plugins/ShacraftTerrain/maps'
stage = ROOT / '.runtime/foundations-stage02'
parser.add_argument('--before-map', type=Path, default=maps / 'foundations-before.json')
parser.add_argument('--after-map', type=Path, default=maps / 'foundations-after.json')
parser.add_argument('--snapshot', type=Path, default=stage / 'before.json.gz')
parser.add_argument('--layout', type=Path, default=stage / 'foundations.json')
parser.add_argument('--report', type=Path, default=stage / 'surface-verification.json')
parser.add_argument('--png', type=Path)
parser.add_argument('--self-test', action='store_true')
args = parser.parse_args()
if args.self_test:
result = unittest.TextTestRunner().run(unittest.defaultTestLoader.loadTestsFromTestCase(FoundationMapTests))
raise SystemExit(0 if result.wasSuccessful() else 1)
before, after, layout = [json.loads(p.read_text()) for p in (args.before_map, args.after_map, args.layout)]
snapshot = survey.load_snapshot(args.snapshot, survey.scope_of(layout['scope']))
report = verify(before, after, snapshot, layout)
report['inputs_sha256'] = {name: hashlib.sha256(path.read_bytes()).hexdigest()
for name, path in [('before_map', args.before_map), ('after_map', args.after_map),
('baseline_voxels', args.snapshot), ('desired_layout', args.layout)]}
args.report.parent.mkdir(parents=True, exist_ok=True)
survey.terrain.save(args.report, report)
if args.png:
render(after, layout, report, args.png)
print(json.dumps({k: v for k, v in report.items() if k not in ('inputs_sha256', 'mismatch_examples', 'note', 'observed_water_voxels_replaced')}, indent=2))
raise SystemExit(0 if report['passed'] else 1)
if __name__ == '__main__':
main()
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Compare every live map column to the planned visible result and inspect hidden water."""
import argparse
from collections import defaultdict
import importlib.util
import json
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--before',type=Path,required=True);p.add_argument('--after',type=Path,required=True)
p.add_argument('--layout',type=Path,action='append',required=True);p.add_argument('--report',type=Path,required=True)
args=p.parse_args();before=json.loads(args.before.read_text());after=json.loads(args.after.read_text())
for key in ('world_uuid','min_x','max_x','min_z','max_z','width','length'):
if before[key]!=after[key]:raise ValueError('Map scopes differ: '+key)
w=before['width'];expected_y=before['surface_y'].copy()
expected_m=[before['palette'][i] for i in before['material_index']]
original_m=expected_m.copy();targets=set();planned_blocks=0
for path in args.layout:
layout=json.loads(path.read_text())
if layout['scope']['world_id']!=before['world_uuid']:raise ValueError('Layout world differs')
for b in sorted(layout['blocks'],key=lambda b:b['y']):
i=(b['z']-before['min_z'])*w+b['x']-before['min_x'];targets.add((b['x'],b['y'],b['z']));planned_blocks+=1
if b['y']>=expected_y[i]:expected_y[i]=b['y'];expected_m[i]=b['block']
observed=[after['palette'][i] for i in after['material_index']]
mismatches=[i for i in range(len(expected_y)) if expected_y[i]!=after['surface_y'][i] or expected_m[i]!=observed[i]]
if mismatches:raise ValueError(f'{len(mismatches)} unexpected surface columns; first indices {mismatches[:8]}')
hidden_water=defaultdict(list)
for i,material in enumerate(original_m):
if material=='minecraft:water' and observed[i]!='minecraft:water':
x=i%w+before['min_x'];z=i//w+before['min_z'];y=before['surface_y'][i]
hidden_water[(x//16,z//16,y)].append((x,y,z))
spec=importlib.util.spec_from_file_location('terrain',ROOT/'scripts/terrain.py')
terrain=importlib.util.module_from_spec(spec);spec.loader.exec_module(terrain)
backend=terrain.Backend(ROOT/'.runtime/server/plugins/MinecraftBuilderMCP/config.yml')
context=backend.call('project_context')
if context['world_id']!=before['world_uuid']:raise ValueError('Live verification world differs')
verified_water=0
for points in hidden_water.values():
lo={axis:min(point[j] for point in points) for j,axis in enumerate(('x','y','z'))}
hi={axis:max(point[j] for point in points) for j,axis in enumerate(('x','y','z'))}
read=backend.call('region_inspect',min=lo,max=hi,detail='blocks')
states={tuple(b['pos'][axis] for axis in ('x','y','z')):b['state'] for b in read['blocks']}
for point in points:
if not states[point].startswith('minecraft:water['):raise ValueError('Water changed beneath a marker: '+str(point))
verified_water+=1
report={'world':before['world'],'source':'live Paper surface maps + bounded RPC water reads',
'verified_surface_columns':len(expected_y),'surface_mismatches':0,
'unique_marker_blocks':len(targets),'planned_writes':planned_blocks,
'original_water_columns':original_m.count('minecraft:water'),'water_columns_obscured_by_markers':verified_water,
'water_obscured_by_markers_verified_intact':True,
'height_unchanged_columns':sum(a==b for a,b in zip(before['surface_y'],after['surface_y'])),
'capture_started_at':after['capture_started_at'],'capture_finished_at':after['capture_finished_at'],
'atomic_snapshot':False,'note':'World edits were idle during each capture. Terrain follows its original heights; raised markers represent future structures, not finished traversable paths.'}
args.report.parent.mkdir(parents=True,exist_ok=True);args.report.write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report))
if __name__=='__main__':main()
+375
View File
@@ -0,0 +1,375 @@
#!/usr/bin/env python3
"""Read-only candidate/live QA for the composed Shacraft arrival gardens.
Candidate mode overlays desired states on the observed baseline. Live mode uses
an observed after snapshot and compares every planned state exactly. The checks
cover this toolkit's static single flowers, persistent leaves, rooted log trees,
supported lanterns and supplied walking surfaces, not arbitrary Minecraft physics.
"""
import argparse
from collections import Counter, deque
import hashlib
import importlib.util
import json
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
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('plaza_qa_survey', ROOT / 'scripts/foundation-survey.py')
navigation = module('plaza_qa_navigation', ROOT / 'scripts/foundation-study/verify_geometry.py')
FLOWERS = {'allium', 'oxeye_daisy', 'azure_bluet', 'pink_tulip', 'white_tulip'}
SOIL = {'dirt', 'grass_block'}
STATIC_CUBES = survey.FULL | {'waxed_oxidized_cut_copper'}
N3 = ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1))
def parse(state):
name, _, raw = state.removeprefix('minecraft:').partition('[')
props = dict(part.split('=', 1) for part in raw.rstrip(']').split(',') if '=' in part)
return name, props
class Reader:
def __init__(self, snapshot, overlay=None):
self.snapshot, self.scope = snapshot, snapshot.scope
self.overlay, self.cache = overlay or {}, {}
def state(self, x, y, z):
key = (x, y, z)
if key not in self.cache:
self.cache[key] = self.overlay[key] if key in self.overlay else self.snapshot.state(x, y, z)
return self.cache[key]
def supported_center(state, face):
"""Conservative face support for full cubes and simple dry shapes used here."""
name, props = parse(state)
if props.get('waterlogged') == 'true':
return False
if name in STATIC_CUBES:
return True
if name.endswith('_slab'):
return props.get('type') == 'double' or props.get('type') == ('bottom' if face == 'down' else 'top')
if name.endswith('_stairs'):
return props.get('half') == ('bottom' if face == 'down' else 'top')
if name == 'iron_chain':
return props.get('axis') == 'y'
if name in ('spruce_fence', 'iron_bars'):
return True
if name == 'stone_brick_wall':
return props.get('up') == 'true'
return False
def leaf_distance(reader, start):
"""Shortest face-connected path from a leaf to a log, capped at seven.
Read actual adjacent states, including unchanged leaves/logs, rather than
trusting the compiler's desired-only propagation or saved distance values.
"""
pending, seen = deque([(start, 0)]), {start}
while pending:
(x, y, z), distance = pending.popleft()
if distance >= 6:
continue
for dx, dy, dz in N3:
neighbor = x + dx, y + dy, z + dz
name, _ = parse(reader.state(*neighbor))
if name.endswith('_log'):
return distance + 1
if name.endswith('_leaves') and neighbor not in seen:
seen.add(neighbor)
pending.append((neighbor, distance + 1))
return 7
def check_fixtures(reader, desired, metadata):
failures, counts = [], Counter()
for at, planned in desired.items():
name, _ = parse(planned)
if name not in FLOWERS and name != 'lantern' and not name.endswith('_leaves'):
continue
x, y, z = at
state = reader.state(*at)
actual_name, props = parse(state)
if actual_name != name:
failures.append({'at': list(at), 'reason': 'fixture_material_differs', 'planned': planned, 'actual': state})
continue
if name in FLOWERS:
counts['flowers'] += 1
below = reader.state(x, y - 1, z)
if parse(below)[0] not in SOIL:
failures.append({'at': list(at), 'reason': 'flower_without_valid_soil', 'below': below})
elif name == 'lantern':
counts['lanterns'] += 1
hanging = props.get('hanging') == 'true'
support = reader.state(x, y + (1 if hanging else -1), z)
if props.get('waterlogged') != 'false' or not supported_center(support, 'down' if hanging else 'up'):
failures.append({'at': list(at), 'reason': 'lantern_without_dry_center_support', 'support': support})
else:
counts['leaves'] += 1
expected_distance = leaf_distance(reader, at)
if (props.get('persistent') != 'true' or props.get('waterlogged') != 'false'
or props.get('distance') != str(expected_distance)):
failures.append({'at': list(at), 'reason': 'unstable_or_wrong_leaf_state',
'actual': state, 'expected_distance': expected_distance})
roots = set()
for fixture in metadata['fixtures']:
if fixture['type'] not in ('conifer', 'topiary'):
continue
counts['rooted_trees'] += 1
x, z = fixture['x'], fixture['z']
root = (x, 96, z)
roots.add(root)
root_name, root_props = parse(reader.state(*root))
below = reader.state(x, 95, z)
if not root_name.endswith('_log') or root_props.get('axis') != 'y' or parse(below)[0] not in SOIL:
failures.append({'at': list(root), 'reason': 'tree_root_without_vertical_log_and_soil', 'below': below})
elif parse(below)[0] == 'grass_block':
failures.append({'at': [x, 95, z], 'reason': 'grass_under_opaque_trunk_will_decay_to_dirt'})
trunk = sorted(p[1] for p, state in desired.items() if p[0] == x and p[2] == z and parse(state)[0].endswith('_log'))
if not trunk or trunk != list(range(96, max(trunk) + 1)):
failures.append({'at': list(root), 'reason': 'discontinuous_planned_trunk', 'log_y': trunk})
else:
for y in trunk:
material, props = parse(reader.state(x, y, z))
if not material.endswith('_log') or props.get('axis') != 'y':
failures.append({'at': [x, y, z], 'reason': 'trunk_gap_or_wrong_axis'})
log_positions = {p for p, s in desired.items() if parse(s)[0].endswith('_log')}
pending, connected = deque(roots & log_positions), roots & log_positions
while pending:
x, y, z = pending.popleft()
for dx, dy, dz in N3:
p = x + dx, y + dy, z + dz
if p in log_positions and p not in connected:
pending.append(p)
connected.add(p)
if log_positions - connected:
failures.append({'reason': 'logs_disconnected_from_declared_tree_roots', 'positions': [list(p) for p in sorted(log_positions - connected)[:20]]})
counts['root_connected_logs'] = len(connected)
return {'passed': not failures, 'checked': dict(counts), 'failures': failures}
def audit(before, layout, metadata, walk, base_navigation, after=None):
scope = survey.scope_of(layout['scope'])
if any(value != scope for value in (before.scope, metadata['scope'], walk['scope'])) or (after and after.scope != scope):
raise ValueError('Project/world/epoch differs between QA inputs')
desired, mismatches = {}, []
for block in layout['blocks']:
pos = survey.point(block)
at = tuple(pos[a] for a in ('x', 'y', 'z'))
if at in desired:
raise ValueError('Duplicate desired coordinate')
if before.state(*at) != block['expected']:
raise ValueError(f'Plan expected state differs from baseline at {at}')
desired[at] = block['block']
if after and after.state(*at) != block['block']:
mismatches.append({'at': list(at), 'expected': block['block'], 'actual': after.state(*at)})
reader = Reader(after or before, None if after else desired)
fixtures = check_fixtures(reader, desired, metadata)
walking = survey.verify_walkable(reader, walk['points'])
nav = json.loads(json.dumps(base_navigation))
blocked = {tuple(p) for p in metadata['unwalkable_columns']}
if any((p['x'], p['z']) in blocked for p in walk['points']):
raise ValueError('Walking samples include declared unwalkable ground')
excluded = 0
for cell in nav['cells']:
if cell['clear'] and (cell['x'], cell['z']) in blocked:
cell['clear'] = False
excluded += 1
navigation_result = navigation.audit(nav)
navigation_result['decorated_ground_columns_excluded'] = excluded
nav_cells = navigation.normalize_cells(nav)
graph = navigation.surface_graph(nav_cells)
reached = navigation.reachable(graph, (0, 9))
bench_access = []
vectors = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)}
for fixture in metadata['fixtures']:
if fixture['type'] != 'bench':
continue
dx, dz = vectors[fixture['facing']]
for offset in (-1, 0, 1):
point = fixture['x'] + dx - dz * offset, fixture['z'] + dz + dx * offset
bench_access.append(navigation.endpoint_report(
f"bench-{fixture['x']}-{fixture['z']}-front-{offset}", point, graph, reached))
navigation_result['bench_front_access'] = bench_access
navigation_result['passed'] &= all(point['passed'] for point in bench_access)
# A canopy stays traversable when it does not occupy the 1.8-block body space.
# Only explicit low obstacles are removed; overhead leaves do not mask paths.
return {'version': 1, 'mode': 'actual_after_snapshot' if after else 'candidate_overlay', 'scope': scope,
'world_edits': 0, 'desired_blocks': len(desired), 'baseline_expected_states_verified': len(desired),
'actual_exact_state_mismatches': len(mismatches) if after else None,
'actual_mismatch_examples': mismatches[:30], 'fixtures': fixtures, 'walking': walking,
'navigation': navigation_result,
'passed': not mismatches and fixtures['passed'] and walking['passed'] and navigation_result['passed'],
'note': 'Static support, leaf-distance and point-sampled navigation checks; not a complete moving-player collision simulation. Actual mode uses sequential observed snapshots.'}
def render_map(document, metadata, report, path, boundary_columns=None, boundary_note=None):
"""Render actual captured surface cells with semantic material colors."""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.colors import to_rgb
from matplotlib.collections import LineCollection
if document.get('source') != 'paper_world_surface' or document.get('world_uuid') != metadata['scope']['world_id']:
raise ValueError('Rendering requires an actual Paper map of this project world')
semantic = {'minecraft:smooth_sandstone': '#e8ddbd', 'minecraft:cut_sandstone': '#d5c7a5',
'minecraft:smooth_sandstone_slab': '#e8ddbd', 'minecraft:grass_block': '#87a75f',
'minecraft:oak_leaves': '#78924d', 'minecraft:spruce_leaves': '#3e6851',
'minecraft:spruce_log': '#765637', 'minecraft:green_concrete': '#548338',
'minecraft:spruce_fence': '#7b5839', 'minecraft:spruce_stairs': '#967147',
'minecraft:allium': '#b388ce', 'minecraft:oxeye_daisy': '#faf3d6',
'minecraft:azure_bluet': '#f2f0db', 'minecraft:pink_tulip': '#efa8b7',
'minecraft:white_tulip': '#f7f3e6', 'minecraft:lantern': '#ecc565'}
palette = []
for name, fallback in zip(document['palette'], document['palette_rgb']):
color = '#68a494' if 'waxed_oxidized_cut_copper' in name else semantic.get(name, fallback)
palette.append(to_rgb(color))
heights = np.asarray(document['surface_y'], dtype=float).reshape(document['length'], document['width'])
indices = np.asarray(document['material_index']).reshape(heights.shape)
rgb = np.asarray(palette)[indices]
dz, dx = np.gradient(heights)
shade = np.clip(.79 + .26 * (.55 * dx + .55 * dz + .63) / np.sqrt(1 + dx * dx + dz * dz), .64, 1.06)
for index, name in enumerate(document['palette']):
if name.removeprefix('minecraft:') in FLOWERS or name.endswith('_concrete'):
shade[indices == index] = 1
rgb = np.clip(rgb * shade[:, :, None], 0, 1)
fig, ax = plt.subplots(figsize=(11, 12), facecolor='#f3f0e6')
ax.imshow(rgb, interpolation='nearest', origin='upper',
extent=(document['min_x'], document['max_x'] + 1, document['max_z'] + 1, document['min_z']))
if boundary_columns:
boundary = set(map(tuple, boundary_columns))
segments = [[(x + .5, z + .5), (x + dx + .5, z + dz + .5)]
for x, z in boundary for dx, dz in ((1, 0), (0, 1)) if (x + dx, z + dz) in boundary]
ax.add_collection(LineCollection(segments, colors='#a74640', linewidths=1, alpha=.85, zorder=5))
ax.scatter([x + .5 for x, z in boundary], [z + .5 for x, z in boundary],
s=2, color='#a74640', alpha=.85, zorder=5, label='Invisible wall · observed barrier blocks')
ax.legend(loc='upper left', fontsize=8, facecolor='#faf7ed', framealpha=.94, edgecolor='#bcbfac')
ax.set(xlim=(-52, 53), ylim=(67, -47), xlabel='X · east →', ylabel='Z (positive south)')
ax.set_aspect('equal')
ax.tick_params(colors='#596451', labelsize=9)
for spine in ax.spines.values():
spine.set_color('#9ca28d')
ax.annotate('N', xy=(47, -44), xytext=(47, -35), ha='center', va='center',
fontsize=11, weight='bold', color='#254534', arrowprops=dict(arrowstyle='-|>', color='#254534'))
ax.plot([-47, -37], [62, 62], color='#254534', lw=2)
ax.text(-42, 60, '10 blocks', ha='center', fontsize=8, color='#254534',
bbox=dict(fc='#faf7ed', ec='none', pad=2, alpha=.9))
fig.suptitle('SHACRAFT / ARRIVAL GARDEN 01', x=.5, y=.975, fontsize=17, weight='bold', color='#244732')
fig.text(.5, .945, 'Captured server surface · original brand inlay · planted gardens',
ha='center', fontsize=10, color='#586751')
fig.text(.5, .035, f"{metadata['planters']} garden beds · {metadata['trees']} conifers · {metadata['benches']} benches · {metadata['new_lamps']} copper-capped lamps\n"
'Observed block positions and materials · semantic colors and height shading.'
+ ('\n' + boundary_note if boundary_note else ''),
ha='center', fontsize=9, color='#586751', linespacing=1.7)
fig.subplots_adjust(left=.09, right=.97, top=.917, bottom=.135 if boundary_note else .105)
path.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(path, dpi=180, facecolor=fig.get_facecolor())
plt.close(fig)
class FakeSnapshot:
scope = {'project_id': 'test', 'world_id': 'world', 'world_epoch': 'epoch'}
def __init__(self, states):
self.states = states
def state(self, x, y, z):
return self.states.get((x, y, z), 'minecraft:air')
class PlazaQATests(unittest.TestCase):
def test_flower_soil_and_hanging_lantern_support(self):
desired = {(0, 96, 0): 'minecraft:white_tulip',
(2, 100, 0): 'minecraft:lantern[hanging=true,waterlogged=false]'}
states = {**desired, (0, 95, 0): 'minecraft:grass_block[snowy=false]', (2, 101, 0): 'minecraft:waxed_oxidized_cut_copper'}
result = check_fixtures(Reader(FakeSnapshot(states)), desired, {'fixtures': []})
self.assertTrue(result['passed'])
del states[(2, 101, 0)]
states[(0, 95, 0)] = 'minecraft:smooth_sandstone'
result = check_fixtures(Reader(FakeSnapshot(states)), desired, {'fixtures': []})
self.assertEqual({f['reason'] for f in result['failures']}, {'flower_without_valid_soil', 'lantern_without_dry_center_support'})
def test_leaf_distance_uses_unchanged_neighbors_and_requires_persistence(self):
leaf = 'minecraft:spruce_leaves[distance=2,persistent=true,waterlogged=false]'
desired = {(0, 100, 0): leaf}
states = {**desired, (1, 100, 0): 'minecraft:oak_leaves[distance=1,persistent=true,waterlogged=false]',
(2, 100, 0): 'minecraft:spruce_log[axis=y]'}
result = check_fixtures(Reader(FakeSnapshot(states)), desired, {'fixtures': []})
self.assertTrue(result['passed'])
states[(0, 100, 0)] = leaf.replace('persistent=true', 'persistent=false')
self.assertFalse(check_fixtures(Reader(FakeSnapshot(states)), desired, {'fixtures': []})['passed'])
def test_disconnected_tree_trunk_is_rejected(self):
desired = {(0, 96, 0): 'minecraft:spruce_log[axis=y]', (0, 98, 0): 'minecraft:spruce_log[axis=y]'}
states = {**desired, (0, 95, 0): 'minecraft:dirt'}
result = check_fixtures(Reader(FakeSnapshot(states)), desired, {'fixtures': [{'type': 'conifer', 'x': 0, 'z': 0}]})
self.assertFalse(result['passed'])
self.assertIn('discontinuous_planned_trunk', {f['reason'] for f in result['failures']})
def test_grass_under_opaque_root_is_unstable_but_dirt_is_valid(self):
desired = {(0, 96, 0): 'minecraft:spruce_log[axis=y]'}
states = {**desired, (0, 95, 0): 'minecraft:grass_block[snowy=false]'}
metadata = {'fixtures': [{'type': 'conifer', 'x': 0, 'z': 0}]}
result = check_fixtures(Reader(FakeSnapshot(states)), desired, metadata)
self.assertEqual(result['failures'][0]['reason'], 'grass_under_opaque_trunk_will_decay_to_dirt')
states[(0, 95, 0)] = 'minecraft:dirt'
self.assertTrue(check_fixtures(Reader(FakeSnapshot(states)), desired, metadata)['passed'])
def main():
parser = argparse.ArgumentParser(description=__doc__)
stage = ROOT / '.runtime/plaza-stage03'
parser.add_argument('--before', type=Path, default=stage / 'before.json.gz')
parser.add_argument('--after', type=Path)
parser.add_argument('--layout', type=Path, default=stage / 'plaza.json')
parser.add_argument('--metadata', type=Path, default=stage / 'plaza.metadata.json')
parser.add_argument('--walk', type=Path, default=stage / 'plaza.walk.json')
parser.add_argument('--navigation', type=Path, default=ROOT / '.runtime/foundations-stage02/navigation-verified-walkable-input.json')
parser.add_argument('--report', type=Path, default=stage / 'candidate-qa.json')
parser.add_argument('--map', type=Path, help='Actual after surface map, used only with --png and --after')
parser.add_argument('--png', type=Path, help='Focused map of the captured arrival garden')
parser.add_argument('--self-test', action='store_true')
args = parser.parse_args()
if args.self_test:
result = unittest.TextTestRunner().run(unittest.defaultTestLoader.loadTestsFromTestCase(PlazaQATests))
raise SystemExit(0 if result.wasSuccessful() else 1)
layout, metadata, walk, nav = [json.loads(p.read_text()) for p in (args.layout, args.metadata, args.walk, args.navigation)]
before = survey.load_snapshot(args.before, layout['scope'])
after = survey.load_snapshot(args.after, layout['scope']) if args.after else None
report = audit(before, layout, metadata, walk, nav, after)
paths = {'before': args.before, 'layout': args.layout, 'metadata': args.metadata, 'walk': args.walk, 'navigation': args.navigation}
if args.after:
paths['after'] = args.after
report['input_sha256'] = {key: hashlib.sha256(path.read_bytes()).hexdigest() for key, path in paths.items()}
args.report.parent.mkdir(parents=True, exist_ok=True)
survey.terrain.save(args.report, report)
if args.png:
if not args.map or not args.after:
parser.error('--png requires an actual --map and --after snapshot')
render_map(json.loads(args.map.read_text()), metadata, report, args.png)
print(json.dumps({'passed': report['passed'], 'mode': report['mode'], 'desired_blocks': report['desired_blocks'],
'exact_mismatches': report['actual_exact_state_mismatches'],
'fixtures': report['fixtures'], 'walk_points': report['walking']['checked_points'],
'walk_failures': report['walking']['failures'][:20],
'navigation_passed': report['navigation']['passed'],
'unreachable_clear_columns': len(report['navigation']['unreachable_clear_columns']),
'route_failures': [r['id'] for r in report['navigation']['routes'] if not r['passed']],
'report': str(args.report.resolve())}, indent=2))
raise SystemExit(0 if report['passed'] else 1)
if __name__ == '__main__':
main()
+389
View File
@@ -0,0 +1,389 @@
#!/usr/bin/env python3
"""Read-only candidate/live station block, public-floor and containment QA.
Recipes use the checked layout contract {version:1,scope,blocks:[x,y,z,block,
expected]}. Metadata declares public_floors, air-only clear_regions and optional
containment bounds/seeds/authorized_caps. Flooding treats partial/unknown shapes
as passable, so a successful result does not rely on decorative collision shapes.
"""
import argparse
from collections import Counter, deque
import hashlib
import importlib.util
import json
import math
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location('station_base', ROOT / 'scripts/verify-balustrade.py')
base = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(base)
survey, plaza = base.survey, base.plaza
FULL = plaza.STATIC_CUBES | {'barrier'}
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))
def full_cube(state):
return plaza.parse(state)[0] in FULL
def bounded_box(document):
box = survey.box_of(document['min'], document['max'])
if survey.volume(box) > survey.MAX_VOXELS:
raise ValueError('Verification box exceeds the snapshot voxel limit')
return box
def positions(box):
return ((x, y, z) for x in range(box['min']['x'], box['max']['x'] + 1)
for y in range(box['min']['y'], box['max']['y'] + 1)
for z in range(box['min']['z'], box['max']['z'] + 1))
def inside(at, box):
return all(box['min'][axis] <= value <= box['max'][axis]
for axis, value in zip(('x', 'y', 'z'), at))
def surface_shape(state):
if full_cube(state):
return [(0., 1.)]
name, props = plaza.parse(state)
if name in {'smooth_sandstone_slab', 'cut_sandstone_slab', 'waxed_oxidized_cut_copper_slab'}:
if props.get('waterlogged') != 'false':
return None
return {'bottom': [(0., .5)], 'top': [(.5, 1.)], 'double': [(0., 1.)]}.get(props.get('type'))
return survey.vertical_shape(state)
def public_floors(reader, floors):
reports = []
for floor in floors:
name, feet, raw = floor['id'], floor['standing_y'], floor['clear_columns']
if feet not in (99, 113):
raise ValueError('This station stage declares public feet at Y99 or Y113')
if (not isinstance(raw, list) or not raw or any(not isinstance(p, list) or len(p) != 2
or any(type(v) is not int for v in p) for p in raw)):
raise ValueError('Each public floor requires integer clear_columns [x,z]')
clear = set(map(tuple, raw))
if len(clear) != len(raw):
raise ValueError('Duplicate public floor column')
source = floor['source']['x'], floor['source']['z']
if source not in clear:
raise ValueError('Floor source is not a declared clear column')
height = floor.get('min_headroom', 4)
if type(height) not in (int, float) or not math.isfinite(height) or height < 1.8:
raise ValueError('Headroom must be finite and at least 1.8 blocks')
failures, valid = [], set()
for x, z in sorted(clear):
support = surface_shape(reader.state(x, feet - 1, z))
reason = None
if support is None or not any(abs(high - 1.) < 1e-8 for low, high in support):
reason = 'missing_or_unknown_support_at_public_feet'
else:
for y in range(feet, math.ceil(feet + height)):
shape = surface_shape(reader.state(x, y, z))
if shape is None:
reason = 'unknown_shape_in_required_clearance'
break
if any(y + lo < feet + height and y + hi > feet for lo, hi in shape):
reason = 'occupied_required_clearance'
break
if reason:
failures.append({'x': x, 'z': z, 'standing_y': feet, 'reason': reason})
else:
valid.add((x, z))
reached, queue = set(), deque([source])
while queue:
p = queue.popleft()
if p in reached or p not in valid:
continue
reached.add(p)
queue.extend((p[0] + dx, p[1] + dz) for dx, dz in N2)
unreachable = sorted(clear - reached)
reports.append({'id': name, 'standing_y': feet, 'required_headroom': height,
'declared_columns': len(clear), 'physically_clear_columns': len(valid),
'reachable_clear_columns': len(reached), 'failed_samples': len(failures),
'sample_failure_examples': failures[:30], 'unreachable_columns': len(unreachable),
'unreachable_examples': unreachable[:30], 'passed': not failures and not unreachable})
return {'status': 'checked' if reports else 'not_provided', 'passed': bool(reports) and all(r['passed'] for r in reports),
'floors': reports, 'method': 'Observed center supports and declared vertical headroom, then cardinal traversal across valid level-floor columns. No lift transitions are inferred.'}
def clear_regions(reader, regions):
reports = []
for region in regions:
box, failures, checked = bounded_box(region), [], 0
for at in positions(box):
checked += 1
state = reader.state(*at)
if state not in survey.AIR:
failures.append({'at': list(at), 'actual': state})
reports.append({'id': region['id'], 'bounds': box, 'checked_voxels': checked,
'nonair_voxels': len(failures), 'examples': failures[:30], 'passed': not failures})
return {'status': 'checked' if reports else 'not_provided', 'passed': bool(reports) and all(r['passed'] for r in reports),
'regions': reports, 'method': 'Every declared doorway/cabin/aisle clearance voxel must be observed air.'}
def named_access(reader, metadata):
"""Ensure furnishing targets are not silently filtered out of public topology."""
interior = metadata.get('interior')
if interior is None:
return {'status': 'not_provided', 'passed': True}
failures, targets_by_floor, lift_reports = [], {}, []
floors = {f['standing_y']: set(map(tuple, f['clear_columns'])) for f in metadata['public_floors']}
for raw_feet, navigation in interior['navigation'].items():
feet = int(raw_feet)
targets = navigation['named_targets']
targets_by_floor[raw_feet] = len(targets)
if feet not in floors or not targets:
failures.append({'reason': 'missing_floor_or_named_targets', 'standing_y': feet})
for target in targets:
x, z = target['x'], target['z']
if target['y'] != feet or (x, z) not in floors.get(feet, set()):
failures.append({'reason': 'named_target_missing_from_public_topology', 'target': target})
if not full_cube(reader.state(x, feet - 1, z)) or any(
reader.state(x, y, z) not in survey.AIR for y in range(feet, feet + 4)):
failures.append({'reason': 'named_target_missing_full_support_or_four_air', 'target': target})
for level, lift in interior['lift'].items():
selector = tuple(lift['selector_block'])
landing = tuple(lift['landing_block'])
selector_state = reader.state(*selector)
x, feet, z = landing
passed = selector_state == 'minecraft:gold_block' and full_cube(reader.state(x, feet - 1, z)) and all(
reader.state(x, y, z) in survey.AIR for y in range(feet, feet + 4))
lift_reports.append({'floor': level, 'selector': selector, 'observed_selector': selector_state,
'landing': landing, 'passed': passed})
if not passed:
failures.append({'reason': 'lift_selector_or_landing_obstructed', 'floor': level})
return {'status': 'checked', 'passed': not failures, 'named_targets_per_floor': targets_by_floor,
'lift_landings': lift_reports, 'failures': failures,
'method': 'Named furnishing targets must remain in the independently verified public-floor topology with full support and four air blocks. Gold selector and landing voxels are checked; runtime lift operation is not inferred.'}
def fixture_stability(reader, desired):
fixtures = {p: s for p, s in desired.items() if plaza.parse(s)[0] in {'lantern', 'spruce_leaves'}}
report = plaza.check_fixtures(reader, fixtures, {'fixtures': []})
report['checked'].pop('root_connected_logs', None)
logs = {p: s for p, s in desired.items() if plaza.parse(s)[0] == 'spruce_log'}
roots = [p for p in logs if (p[0], p[1] - 1, p[2]) not in logs]
for x, y, z in roots:
below = reader.state(x, y - 1, z)
if plaza.parse(below)[0] not in {'dirt', 'moss_block'}:
report['failures'].append({'reason': 'station_topiary_or_diorama_root_without_soil',
'at': [x, y, z], 'below': below})
report['checked'].update({'spruce_logs': len(logs), 'topiary_or_diorama_roots': len(roots)})
report['passed'] = not report['failures']
report['status'] = 'checked'
report['method'] = 'Planned lantern support, persistent leaf distances against the full observed neighborhood, and station topiary/diorama roots on stable dirt or moss.'
return report
def conservative_containment(reader, metadata):
if metadata is None:
return {'status': 'not_proven', 'passed': False, 'reason': 'No containment bounds, seeds and authorized entrance caps were supplied.'}
box = bounded_box(metadata['bounds'])
caps, cap_reports = set(), []
for region in metadata.get('authorized_caps', []):
cap_box = bounded_box(region)
points = set(positions(cap_box))
if not points or not all(inside(p, box) for p in points):
raise ValueError('Authorized virtual cap lies outside surveyed containment bounds')
caps |= points
cap_reports.append({'id': region['id'], 'bounds': cap_box, 'voxels': len(points)})
blocked, passable, unknown = set(), set(), Counter()
for at in positions(box):
state = reader.state(*at)
if full_cube(state) or at in caps:
blocked.add(at)
else:
passable.add(at)
if state not in survey.AIR:
unknown[plaza.parse(state)[0]] += 1
regions = [(r['id'], bounded_box(r)) for r in metadata.get('forbidden_regions', [])]
roof_y = metadata.get('forbidden_y_at_or_above', 126)
if type(roof_y) is not int:
raise ValueError('Forbidden roof threshold must be an integer Y')
seeds = metadata.get('seeds')
if not isinstance(seeds, list) or not seeds:
raise ValueError('Containment requires explicit public aisle/cabin seeds')
reports = []
for seed in seeds:
coordinates = [seed[axis] for axis in ('x', 'y', 'z')]
if any(type(v) not in (int, float) or not math.isfinite(v) for v in coordinates):
raise ValueError('Seed coordinates must be finite')
source = tuple(math.floor(v) for v in coordinates)
if not inside(source, box) or source in caps:
raise ValueError('Containment source is outside its observed box or inside a virtual cap')
low, high = seed.get('min_y'), seed.get('max_y')
if any(v is not None and type(v) is not int for v in (low, high)):
raise ValueError('Per-floor minimum/maximum reachable Y must be integers')
reached, queue, violations, examples = set(), deque([source]), Counter(), []
while queue:
p = queue.popleft()
if p in reached or p not in passable:
continue
reached.add(p)
reasons = []
if any(value in (box['min'][axis], box['max'][axis]) for axis, value in zip(('x', 'y', 'z'), p)):
reasons.append('survey_boundary_reachable')
if p[1] >= roof_y:
reasons.append('roof_space_reachable')
if low is not None and p[1] < low:
reasons.append('below_public_floor_reachable')
if high is not None and p[1] > high:
reasons.append('above_public_room_reachable')
reasons.extend('forbidden_region:' + name for name, bounds in regions if inside(p, bounds))
for reason in reasons:
violations[reason] += 1
if len(examples) < 30:
examples.append({'at': list(p), 'reason': reason})
queue.extend((p[0] + dx, p[1] + dy, p[2] + dz) for dx, dy, dz in N3)
source_air = reader.state(*source) in survey.AIR
reports.append({'id': seed['id'], 'source': list(source), 'source_observed_air': source_air,
'reachable_voxels': len(reached), 'minimum_reached_y': min((p[1] for p in reached), default=None),
'maximum_reached_y': max((p[1] for p in reached), default=None),
'violation_counts': dict(violations), 'examples': examples,
'passed': source_air and bool(reached) and not violations})
return {'status': 'checked', 'passed': all(r['passed'] for r in reports), 'bounds': box,
'observed_voxels': len(blocked) + len(passable), 'authorized_virtual_caps': cap_reports,
'partial_or_unknown_materials_treated_as_passable': dict(unknown), 'seeds': reports,
'method': 'Independent 6-neighbor floods from each public room/cabin seed. Known full cubes including glass block movement; partial/unknown shapes are treated as empty. Authorized entrance caps are virtual audit boundaries only.',
'limits': 'This overestimates continuous player movement and does not simulate teleportation, spectator, block removal or lift operation. Failed floods through partial shapes require review; they are not automatically proven playable escape paths.'}
def audit(before, recipe, metadata, after=None):
scope = survey.scope_of(recipe['scope'])
if scope != before.scope or scope != metadata['scope'] or (after and after.scope != scope):
raise ValueError('Recipe, metadata and snapshots differ in project/world/epoch')
desired = base.coordinates(recipe)
for row in recipe['blocks']:
at = tuple(row[axis] for axis in ('x', 'y', 'z'))
if before.state(*at) != row['expected']:
raise ValueError(f'Recipe expected state differs from observed baseline at {at}')
reader = plaza.Reader(after or before, None if after else desired)
floor_report = public_floors(reader, metadata.get('public_floors', []))
clearance = clear_regions(reader, metadata.get('clear_regions', []))
access = named_access(reader, metadata)
fixtures = fixture_stability(reader, desired)
containment = conservative_containment(reader, metadata.get('containment'))
volume = base.compare_volume(before, after, desired) if after else None
unproven = []
if floor_report['status'] == 'not_provided':
unproven.append('Public floor support/headroom/reachability: no clear-column topology supplied.')
if clearance['status'] == 'not_provided':
unproven.append('Doorway and lift cabin clearances: no explicit clearance regions supplied.')
if containment['status'] == 'not_proven':
unproven.append('Windows, floor separation and roof/private-space containment: no flood contract supplied.')
provided = [r for r in (floor_report, clearance, access, fixtures, containment) if r['status'] == 'checked']
supported_passed = all(r['passed'] for r in provided) and (volume is None or volume['passed'])
return {'version': 1, 'scope': scope, 'world_edits': 0,
'mode': 'actual_after_snapshot' if after else 'candidate_overlay', 'desired_blocks': len(desired),
'expected_states_verified_against_baseline': len(desired), 'public_floors': floor_report,
'clear_regions': clearance, 'named_access': access, 'fixtures': fixtures,
'containment': containment, 'volume': volume,
'supported_checks_passed': supported_passed, 'unproven_checks': unproven,
'passed': supported_passed and not unproven,
'limits': 'Only observed block states are verified. Block entity text, display entities, gameplay/sign destinations and operational lift transitions require separate runtime checks. Actual snapshots are sequential, not atomic.'}
class FakeReader:
def __init__(self, states):
self.states = states
def state(self, x, y, z):
return self.states.get((x, y, z), 'minecraft:air')
class StationTests(unittest.TestCase):
def room(self):
box = {'min': {'x': -1, 'y': 98, 'z': -1}, 'max': {'x': 5, 'y': 127, 'z': 5}}
states = {(x, y, z): 'minecraft:glass' for x in range(5) for y in range(99, 104) for z in range(5)
if x in (0, 4) or z in (0, 4) or y in (99, 103)}
meta = {'bounds': box, 'seeds': [{'id': 'room', 'x': 2, 'y': 100, 'z': 2, 'min_y': 100, 'max_y': 102}]}
return FakeReader(states), meta
def test_closed_glass_room_is_contained_and_window_hole_is_detected(self):
reader, metadata = self.room()
self.assertTrue(conservative_containment(reader, metadata)['passed'])
del reader.states[0, 101, 2]
result = conservative_containment(reader, metadata)
self.assertFalse(result['passed'])
self.assertIn('survey_boundary_reachable', result['seeds'][0]['violation_counts'])
def test_authorized_entrance_can_be_virtually_capped_without_changing_world(self):
reader, metadata = self.room()
del reader.states[0, 101, 2]
metadata['authorized_caps'] = [{'id': 'entry', 'min': {'x': 0, 'y': 101, 'z': 2}, 'max': {'x': 0, 'y': 101, 'z': 2}}]
self.assertTrue(conservative_containment(reader, metadata)['passed'])
self.assertEqual(reader.state(0, 101, 2), 'minecraft:air')
def test_partial_window_is_treated_as_open_and_upper_floor_hole_is_detected(self):
reader, metadata = self.room()
reader.states[0, 101, 2] = 'minecraft:iron_bars[east=false,north=true,south=true,waterlogged=false,west=false]'
self.assertFalse(conservative_containment(reader, metadata)['passed'])
reader, metadata = self.room()
del reader.states[2, 99, 2]
result = conservative_containment(reader, metadata)
self.assertIn('below_public_floor_reachable', result['seeds'][0]['violation_counts'])
def test_public_headroom_is_four_blocks_and_obstacle_disconnects_floor(self):
reader = FakeReader({(x, 98, 0): 'minecraft:stone' for x in range(3)})
floor = {'id': 'vestibule', 'standing_y': 99, 'source': {'x': 0, 'z': 0}, 'clear_columns': [[0, 0], [1, 0], [2, 0]]}
self.assertTrue(public_floors(reader, [floor])['passed'])
reader.states[1, 102, 0] = 'minecraft:lantern[hanging=true,waterlogged=false]'
result = public_floors(reader, [floor])
self.assertFalse(result['passed'])
self.assertEqual(result['floors'][0]['unreachable_columns'], 2)
def test_clearance_region_reports_real_door_obstruction(self):
reader = FakeReader({(0, 99, 0): 'minecraft:stone'})
region = {'id': 'door', 'min': {'x': 0, 'y': 99, 'z': 0}, 'max': {'x': 0, 'y': 102, 'z': 0}}
self.assertEqual(clear_regions(reader, [region])['regions'][0]['nonair_voxels'], 1)
def test_named_target_cannot_be_hidden_by_filtering_the_public_mask(self):
reader = FakeReader({(x, 98, 0): 'minecraft:stone' for x in range(2)})
metadata = {'public_floors': [{'standing_y': 99, 'clear_columns': [[0, 0]]}],
'interior': {'navigation': {'99': {'named_targets': [
{'name': 'bench-front', 'x': 1, 'y': 99, 'z': 0}]}}, 'lift': {}}}
self.assertFalse(named_access(reader, metadata)['passed'])
metadata['public_floors'][0]['clear_columns'].append([1, 0])
self.assertTrue(named_access(reader, metadata)['passed'])
def test_fixture_leaf_distance_and_lantern_support_use_actual_states(self):
desired = {(0, 100, 0): 'minecraft:lantern[hanging=true,waterlogged=false]',
(2, 100, 0): 'minecraft:spruce_leaves[distance=1,persistent=true,waterlogged=false]'}
reader = FakeReader(desired | {(0, 101, 0): 'minecraft:stone',
(2, 99, 0): 'minecraft:spruce_log[axis=y]'})
self.assertTrue(fixture_stability(reader, desired)['passed'])
del reader.states[0, 101, 0]
del reader.states[2, 99, 0]
self.assertEqual(len(fixture_stability(reader, desired)['failures']), 2)
def main():
parser = argparse.ArgumentParser(description=__doc__)
for name in ('before', 'after', 'recipe', 'metadata', 'report'):
parser.add_argument('--' + name, type=Path)
parser.add_argument('--self-test', action='store_true')
args = parser.parse_args()
if args.self_test:
result = unittest.TextTestRunner().run(unittest.defaultTestLoader.loadTestsFromTestCase(StationTests))
raise SystemExit(0 if result.wasSuccessful() else 1)
if any(getattr(args, name) is None for name in ('before', 'recipe', 'metadata', 'report')):
parser.error('--before, --recipe, --metadata and --report are required')
before = survey.load_snapshot(args.before)
after = survey.load_snapshot(args.after, before.scope) if args.after else None
report = audit(before, json.loads(args.recipe.read_text()), json.loads(args.metadata.read_text()), after)
paths = {name: getattr(args, name) for name in ('before', 'after', 'recipe', 'metadata') if getattr(args, name)}
report['input_sha256'] = {key: hashlib.sha256(path.read_bytes()).hexdigest() for key, path in paths.items()}
args.report.parent.mkdir(parents=True, exist_ok=True)
survey.terrain.save(args.report, report)
print(json.dumps(report, indent=2))
raise SystemExit(0 if report['passed'] else 2 if report['supported_checks_passed'] and report['unproven_checks'] else 1)
if __name__ == '__main__':
main()
+384
View File
@@ -0,0 +1,384 @@
#!/usr/bin/env python3
"""Read-only proof of a closed, observed full-cube Minecraft zone enclosure.
The membrane is derived independently from an extruded XZ mask minus explicit
excluded voxels: every six-neighbor exterior shell voxel must be a full cube. An
exterior flood treats every unknown/partial block as empty, overestimating escape
routes. Swept 0.6 x 1.8 player AABB probes additionally test cardinal/diagonal
crossings, half-block stair rises, floor drops and roof ascent. These sample
probes supplement the complete membrane proof; they are not the proof itself.
This verifies static continuous movement, not spectator, commands, breaking,
plugin teleportation, or arbitrary discontinuous teleport/pearl behavior.
"""
import argparse
from collections import Counter, deque
import hashlib
import importlib.util
import itertools
import json
import math
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location('containment_base', ROOT / 'scripts/verify-balustrade.py')
base = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(base)
survey, plaza = base.survey, base.plaza
FULL = plaza.STATIC_CUBES | {'barrier'}
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))
WIDTH, HEIGHT = .6, 1.8
def full_cube(state):
return plaza.parse(state)[0] in FULL
def mask_geometry(metadata):
raw = metadata['interior_columns']
if (not isinstance(raw, list) or not raw or any(not isinstance(p, list) or len(p) != 2
or any(type(v) is not int for v in p) for p in raw)):
raise ValueError('interior_columns must be nonempty integer [x,z] pairs')
interior = set(map(tuple, raw))
if len(interior) != len(raw):
raise ValueError('Duplicate interior column')
floor, roof = metadata['floor_y'], metadata['roof_y']
if type(floor) is not int or type(roof) is not int or roof - floor < 4:
raise ValueError('Integer floor_y and roof_y need at least three free interior rows')
shell = {(x + dx, z + dz) for x, z in interior for dx, dz in N2} - interior
footprint = interior | shell
reached, queue = set(), deque([next(iter(interior))])
while queue:
p = queue.popleft()
if p in reached or p not in interior:
continue
reached.add(p)
queue.extend((p[0] + dx, p[1] + dz) for dx, dz in N2)
if reached != interior:
raise ValueError('Interior mask must be one cardinally connected component')
for key in ('wall_columns', 'shell_columns'):
if ('interior_excluded_voxels' not in metadata and key in metadata
and set(map(tuple, metadata[key])) != shell):
raise ValueError(f'Claimed {key} differ from the independently derived complete shell')
bounds = ((min(x for x, z in footprint) - 1, floor - 1, min(z for x, z in footprint) - 1),
(max(x for x, z in footprint) + 1, roof + 1, max(z for x, z in footprint) + 1))
if math.prod(hi - lo + 1 for lo, hi in zip(*bounds)) > survey.MAX_VOXELS:
raise ValueError('Enclosure verification exceeds the bounded snapshot voxel limit')
raw_excluded = metadata.get('interior_excluded_voxels', [])
if (not isinstance(raw_excluded, list) or any(not isinstance(p, list) or len(p) != 3
or any(type(v) is not int for v in p) for p in raw_excluded)):
raise ValueError('interior_excluded_voxels must contain integer [x,y,z] triples')
excluded = set(map(tuple, raw_excluded))
if len(excluded) != len(raw_excluded):
raise ValueError('Duplicate excluded interior voxel')
volume = {(x, y, z) for x, z in interior for y in range(floor + 1, roof)}
if not excluded <= volume:
raise ValueError('Excluded voxel is outside the base extruded interior')
volume -= excluded
if not volume:
raise ValueError('Excluded voxels remove the whole enclosure interior')
membrane_voxels = {(x + dx, y + dy, z + dz) for x, y, z in volume for dx, dy, dz in N3} - volume
return interior, shell, footprint, floor, roof, bounds, volume, membrane_voxels, excluded
def membrane(reader, geometry):
interior, _, _, floor, roof, _ = geometry[:6]
failures, materials, counts = [], Counter(), Counter()
for x, y, z in sorted(geometry[7]):
kind = ('floor' if y == floor else 'roof' if y == roof else
'wall' if (x, z) not in interior else 'folded_boundary')
counts[kind] += 1
state = reader.state(x, y, z)
materials[plaza.parse(state)[0]] += 1
if not full_cube(state):
failures.append({'at': [x, y, z], 'part': kind, 'actual': state})
return {'passed': not failures, 'required_voxels': sum(counts.values()), 'parts': dict(counts),
'observed_materials': dict(materials), 'nonfull_voxels': len(failures), 'examples': failures[:30],
'method': 'Every voxel in N6(interior volume) minus interior volume must be an observed full collision cube.'}
def conservative_exterior_flood(reader, geometry, source):
lo, hi = geometry[5]
blocked, passable, unknown = set(), set(), Counter()
boundary = []
for x in range(lo[0], hi[0] + 1):
for y in range(lo[1], hi[1] + 1):
for z in range(lo[2], hi[2] + 1):
p = x, y, z
state = reader.state(*p)
if full_cube(state):
blocked.add(p)
continue
passable.add(p)
if state not in survey.AIR:
unknown[plaza.parse(state)[0]] += 1
if any(v in (low, high) for v, low, high in zip(p, lo, hi)):
boundary.append(p)
reached, queue = set(boundary), deque(boundary)
while queue:
x, y, z = queue.popleft()
for dx, dy, dz in N3:
p = x + dx, y + dy, z + dz
if p in passable and p not in reached:
reached.add(p)
queue.append(p)
leaks = sorted(reached & geometry[6])
source_cell = tuple(math.floor(v) for v in source)
return {'passed': not leaks, 'observed_voxels': len(blocked) + len(passable),
'exterior_reached_voxels': len(reached), 'interior_reached_from_exterior': len(leaks),
'source_reached_from_exterior': source_cell in reached,
'unknown_or_partial_blocks_treated_as_empty': dict(unknown), 'leak_examples': leaks[:30],
'method': 'Six-neighbor free-voxel exterior flood; only known full collision cubes obstruct it.'}
def segment_box(start, finish, low, high):
"""Slab intersection with the interior of an expanded block AABB.
Face contact is legal: a player's feet resting exactly on paving must not
make every horizontal probe appear blocked before reaching the guard.
"""
enter, leave = 0., 1.
for a, b, lo, hi in zip(start, finish, low, high):
lo, hi = lo + 1e-9, hi - 1e-9
delta = b - a
if abs(delta) < 1e-12:
if a < lo or a > hi:
return False
continue
first, last = sorted(((lo - a) / delta, (hi - a) / delta))
enter, leave = max(enter, first), min(leave, last)
if enter > leave:
return False
return True
def swept_player_hits_full_cube(reader, start, finish):
radius = WIDTH / 2
low = [math.floor(min(start[i], finish[i]) - (radius if i != 1 else 0)) for i in range(3)]
high = [math.floor(max(start[i], finish[i]) + (radius if i != 1 else HEIGHT)) for i in range(3)]
for x in range(low[0], high[0] + 1):
for y in range(low[1], high[1] + 1):
for z in range(low[2], high[2] + 1):
if full_cube(reader.state(x, y, z)) and segment_box(
start, finish, (x - radius, y - HEIGHT, z - radius),
(x + 1 + radius, y + 1, z + 1 + radius)):
return True
return False
def player_probes(reader, geometry, source):
interior, _, _, floor, roof, _ = geometry[:6]
counts, missed = Counter(), []
source_clear = all(reader.state(x, y, z) in survey.AIR
for x in range(math.floor(source[0] - WIDTH / 2), math.floor(source[0] + WIDTH / 2) + 1)
for y in range(math.floor(source[1]), math.ceil(source[1] + HEIGHT))
for z in range(math.floor(source[2] - WIDTH / 2), math.floor(source[2] + WIDTH / 2) + 1))
# Cross each boundary in cardinal and diagonal directions, including half-
# block stair rises/drops. Fixed-height samples also cover free flight rows.
heights = sorted({float(floor + 1), float(source[1]), roof - HEIGHT - .05})
for x, z in sorted(interior):
for dx, dz in itertools.product((-1, 0, 1), repeat=2):
if (dx == dz == 0) or (x + dx, z + dz) in interior:
continue
for y in heights:
for dy in (-.5, 0., .5):
start = (x + .5, y, z + .5)
finish = (x + dx + .5, y + dy, z + dz + .5)
kind = 'diagonal' if dx and dz else 'cardinal'
counts[kind] += 1
if not swept_player_hits_full_cube(reader, start, finish):
missed.append({'kind': kind, 'from': start, 'to': finish})
# Test the locally folded boundary as well as the original outer perimeter.
# The complete membrane check covers every height even when a sample starts
# in an already obstructed partial-height pocket.
for x, y, z in sorted(geometry[8] & geometry[7]):
for dx, dy, dz in N3:
p = x + dx, y + dy, z + dz
if p not in geometry[6]:
continue
start, finish = (p[0] + .5, float(p[1]), p[2] + .5), (x + .5, float(y), z + .5)
counts['folded_boundary'] += 1
if not swept_player_hits_full_cube(reader, start, finish):
missed.append({'kind': 'folded_boundary', 'from': start, 'to': finish})
for kind, start, finish in [('floor', (source[0], floor + 1.1, source[2]),
(source[0], floor - .5, source[2])),
('roof', (source[0], roof - HEIGHT - .1, source[2]),
(source[0], roof - .5, source[2]))]:
counts[kind] += 1
if not swept_player_hits_full_cube(reader, start, finish):
missed.append({'kind': kind, 'from': start, 'to': finish})
return {'passed': source_clear and not missed, 'player_width': WIDTH, 'player_height': HEIGHT,
'source_headroom_observed_air': source_clear, 'swept_crossing_probes': dict(counts),
'unblocked_probes': len(missed), 'examples': missed[:30],
'note': 'Supplementary swept-AABB boundary probes; the independently derived full membrane is the complete static containment criterion.'}
def volume_components(reader, geometry, source):
"""Report geometric components; every one is still checked for containment."""
pending, components = set(geometry[6]), []
source_cell = tuple(math.floor(v) for v in source)
while pending:
queue, count, nonfull, contains_source = deque([next(iter(pending))]), 0, 0, False
while queue:
p = queue.popleft()
if p not in pending:
continue
pending.remove(p)
count += 1
nonfull += not full_cube(reader.state(*p))
contains_source |= p == source_cell
queue.extend((p[0] + dx, p[1] + dy, p[2] + dz) for dx, dy, dz in N3)
components.append({'voxels': count, 'nonfull_voxels': nonfull, 'contains_source': contains_source})
return {'components': sorted(components, key=lambda c: c['voxels'], reverse=True),
'source_in_volume': any(c['contains_source'] for c in components),
'note': 'Disconnected sealed components are reported, not treated as escapes. The entire required membrane and exterior flood are checked.'}
def preserved_decor(before, reader, desired):
protected, failures = 0, []
for at, state in base.captured_voxels(before):
name, _ = plaza.parse(state)
if (name not in plaza.FLOWERS | {'lantern', 'spruce_fence', 'spruce_stairs', 'iron_chain'}
and not any(suffix in name for suffix in ('_leaves', '_log', 'waxed_oxidized_cut_copper'))):
continue
protected += 1
if reader.state(*at) != state:
failures.append({'at': list(at), 'before': state, 'after': reader.state(*at), 'in_plan': at in desired})
return {'passed': not failures, 'protected_observed_states': protected, 'changed_states': len(failures),
'examples': failures[:30]}
def audit(before, layout, metadata, after=None):
scope = survey.scope_of(layout['scope'])
if scope != before.scope or scope != metadata['scope'] or (after and after.scope != scope):
raise ValueError('Project/world/epoch differs between containment inputs')
desired = base.coordinates(layout)
for row in layout['blocks']:
p = tuple(row[a] for a in ('x', 'y', 'z'))
if before.state(*p) != row['expected']:
raise ValueError(f'Expected plan state differs from observed baseline at {p}')
source = tuple(metadata['source'][axis] for axis in ('x', 'y', 'z'))
if any(type(v) not in (int, float) or not math.isfinite(v) for v in source):
raise ValueError('Source must contain finite x/y/z player feet coordinates')
geometry = mask_geometry(metadata)
if tuple(math.floor(v) for v in source) not in geometry[6] or not geometry[3] < source[1] < geometry[4] - HEIGHT:
raise ValueError('Source is outside the enclosure interior')
reader = plaza.Reader(after or before, None if after else desired)
closed = membrane(reader, geometry)
flood = conservative_exterior_flood(reader, geometry, source)
probes = player_probes(reader, geometry, source)
components = volume_components(reader, geometry, source)
decor = preserved_decor(before, reader, desired)
volume = base.compare_volume(before, after, desired) if after else None
return {'version': 1, 'scope': scope, 'world_edits': 0,
'mode': 'actual_after_snapshot' if after else 'candidate_overlay',
'desired_blocks': len(desired), 'interior_columns': len(geometry[0]),
'independently_derived_wall_columns': len(geometry[1]),
'interior_voxels': len(geometry[6]), 'interior_excluded_voxels': len(geometry[8]),
'volume_components': components,
'membrane': closed, 'exterior_flood': flood, 'player_probes': probes,
'preserved_decor': decor, 'volume': volume,
'passed': all(r['passed'] for r in (closed, flood, probes, decor)) and (volume is None or volume['passed']),
'limitations': 'Static continuous collision containment for normal nonspectator players only. No protection against spectator, breaking/removing blocks, operator commands, plugin teleports or arbitrary discontinuous teleport/pearl behavior. Actual snapshots are sequential, not atomic.'}
class ContainmentTests(unittest.TestCase):
def scene(self):
meta = {'interior_columns': [[0, 0], [1, 0], [0, 1], [1, 1]], 'floor_y': 0, 'roof_y': 5}
geo = mask_geometry(meta)
states = {}
for x, z in geo[2]:
for y in ([0, 5] if (x, z) in geo[0] else range(6)):
states[x, y, z] = 'minecraft:barrier'
reader = type('Reader', (), {'state': lambda self, x, y, z: states.get((x, y, z), 'minecraft:air')})()
return meta, geo, states, reader
def test_complete_shell_blocks_flight_diagonals_stairs_and_drops(self):
_, geo, _, reader = self.scene()
self.assertTrue(membrane(reader, geo)['passed'])
self.assertTrue(conservative_exterior_flood(reader, geo, (.5, 1., .5))['passed'])
self.assertTrue(player_probes(reader, geo, (.5, 1., .5))['passed'])
def test_one_missing_wall_or_roof_voxel_leaks(self):
for at in [(-1, 2, 0), (0, 5, 0), (0, 0, 0)]:
_, geo, states, reader = self.scene()
del states[at]
self.assertFalse(membrane(reader, geo)['passed'])
self.assertFalse(conservative_exterior_flood(reader, geo, (.5, 1., .5))['passed'])
def test_floor_need_not_extend_under_exterior_wall(self):
_, geo, states, reader = self.scene()
for x, z in geo[1]:
states.pop((x, 0, z), None)
self.assertTrue(membrane(reader, geo)['passed'])
self.assertTrue(conservative_exterior_flood(reader, geo, (.5, 1., .5))['passed'])
def test_inward_fold_preserves_partial_stair_outside_and_missing_fold_leaks(self):
meta, _, states, reader = self.scene()
meta['interior_excluded_voxels'] = [[0, 2, 0]]
geo = mask_geometry(meta)
states[-1, 2, 0] = 'minecraft:stone_brick_stairs[facing=east,half=bottom,shape=straight,waterlogged=false]'
states[0, 2, 0] = 'minecraft:barrier'
self.assertNotIn((-1, 2, 0), geo[7])
self.assertTrue(membrane(reader, geo)['passed'])
self.assertTrue(conservative_exterior_flood(reader, geo, (1.5, 1., 1.5))['passed'])
self.assertTrue(player_probes(reader, geo, (1.5, 1., 1.5))['passed'])
del states[0, 2, 0]
self.assertFalse(membrane(reader, geo)['passed'])
self.assertFalse(conservative_exterior_flood(reader, geo, (1.5, 1., 1.5))['passed'])
def test_exclusions_must_be_unique_voxels_inside_original_volume(self):
meta, _, _, _ = self.scene()
for exclusions in [[[0, 2, 0], [0, 2, 0]], [[100, 2, 100]], [[0, 0, 0]]]:
meta['interior_excluded_voxels'] = exclusions
with self.assertRaises(ValueError):
mask_geometry(meta)
def test_stair_or_slab_cannot_substitute_for_solid_membrane(self):
_, geo, states, reader = self.scene()
states[-1, 2, 0] = 'minecraft:stone_brick_slab[type=bottom,waterlogged=false]'
self.assertFalse(membrane(reader, geo)['passed'])
self.assertFalse(conservative_exterior_flood(reader, geo, (.5, 1., .5))['passed'])
states[-1, 2, 0] = 'minecraft:stone'
self.assertTrue(membrane(reader, geo)['passed'])
def test_swept_aabb_detects_diagonal_corner_and_vertical_collision(self):
reader = type('Reader', (), {'state': lambda self, x, y, z: 'minecraft:barrier' if (x, y, z) == (1, 1, 1) else 'minecraft:air'})()
self.assertTrue(swept_player_hits_full_cube(reader, (.5, 1., .5), (2.5, 1., 2.5)))
self.assertFalse(swept_player_hits_full_cube(reader, (.2, 1., .2), (.2, 2., .2)))
def test_standing_contact_with_floor_does_not_mask_an_open_horizontal_route(self):
reader = type('Reader', (), {'state': lambda self, x, y, z: 'minecraft:stone' if y == 0 else 'minecraft:air'})()
self.assertFalse(swept_player_hits_full_cube(reader, (.5, 1., .5), (2.5, 1., 2.5)))
self.assertTrue(swept_player_hits_full_cube(reader, (.5, 1., .5), (.5, .5, .5)))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--before', type=Path)
parser.add_argument('--after', type=Path)
parser.add_argument('--layout', type=Path)
parser.add_argument('--metadata', type=Path)
parser.add_argument('--report', type=Path)
parser.add_argument('--self-test', action='store_true')
args = parser.parse_args()
if args.self_test:
result = unittest.TextTestRunner().run(unittest.defaultTestLoader.loadTestsFromTestCase(ContainmentTests))
raise SystemExit(0 if result.wasSuccessful() else 1)
if any(getattr(args, key) is None for key in ('before', 'layout', 'metadata', 'report')):
parser.error('--before, --layout, --metadata and --report are required')
before = survey.load_snapshot(args.before)
after = survey.load_snapshot(args.after, before.scope) if args.after else None
report = audit(before, json.loads(args.layout.read_text()), json.loads(args.metadata.read_text()), after)
paths = {key: getattr(args, key) for key in ('before', 'after', 'layout', 'metadata') if getattr(args, key)}
report['input_sha256'] = {key: hashlib.sha256(path.read_bytes()).hexdigest() for key, path in paths.items()}
args.report.parent.mkdir(parents=True, exist_ok=True)
survey.terrain.save(args.report, report)
print(json.dumps(report, indent=2))
raise SystemExit(0 if report['passed'] else 1)
if __name__ == '__main__':
main()