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.
184 lines
9.4 KiB
Python
184 lines
9.4 KiB
Python
#!/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()
|