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