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:
@@ -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()
|
||||
Reference in New Issue
Block a user