* feat(ascii-city-engine): deep enrichment — street furniture, signage, dense Raleigh Enriches the merged v0.1 skill with a dense, real-data streetscape. Schema (backward-compatible): props gain optional label + provenance; new optional signs layer for street-name text; buildings gain name/address/use. world.schema.json admits signs; all v0.1 packs still validate. Validator: per-kind prop glyph map with unknown-kind flag; sign validation; signs included in content-bounds; v0.1 guards (isinstance crash-guard, O(n^2) DoS caps) intact. Engine: spatially-indexed prop billboards (signals, trees, crossings, transit stops, bollards, benches, hydrants) depth-tested at terrain(x,y); perspective-projected street-name sign text as an overlay pass; road surface-material and lit ground styling; crosswalk bands; wayfinding HUD naming the current street and the building faced. Reads spawn + first tile from manifest.json. Raleigh sample regenerated dense (same authoritative bbox): 159 buildings (64 named, addresses, uses), 899 surfaces (86 named, surface/lit/lanes), 298 props across 7 kinds, 29 real street-name signs. 520K, under 2 MB. Docs + evals: contract/engine-architecture/gis-ingestion/raleigh-poc updated for the new layers and acceptance checks; 2 new evals (no fabricated signage; props anchored to terrain). Verified: validator 1410 PASS / 0 FAIL; broken fixture + AttributeError repro exit 1; 5000-vertex DoS rejected in 31 ms; node --check OK; HTTP smoke 200 on engine/manifest/tile; validate-skills.rb 154 green; eval-coverage passes; blocklist clean; headless-Chrome render shows readable in-world 'North Wilmington Street' sign, signals, crosswalks, and on-street HUD. AI-assisted contribution (Hermes Agent, spec-driven-development pipeline). * fix(ascii-city-engine): address droid-review findings on PR #322 1. [P1] facingBuilding(): wrap the heading delta to [-pi,pi] before taking abs, so the Facing: HUD stops reporting a behind-the-camera building after the player turns past ~180 degrees. 2. [P2] buildIndices(): rasterize surface polyline edges into grid cells so long straight road segments register for surface styling and the On: HUD street name, instead of only indexing endpoint vertices. 3. [P1] validator: guard the new props kinds comprehension and the props/ signs loops in all_points() against null, so 'props: null' or 'signs: null' produce a structured FAIL instead of an uncaught TypeError traceback. 4. [P3][security] validator: enforce the previously-declared MAX_FEATURES_PER_TILE (buildings+surfaces+props+signs) to keep the O(n^2) geometry and duplicate-ID paths bounded in aggregate. 5. [P3] engine: paint marked crossings as a distinct ground band (=) instead of a floating billboard, matching the documented crosswalk rendering. 6. [P3] eval: align prop-null-terrain expected_output with the render-time skip behavior actually implemented. Verified: dense pack 1411/0 exit 0; broken/crash/dos/null packs all exit 1 with clean FAIL reports (no tracebacks); JS node --check OK; validate-skills 154 green; blocklist clean. * fix(ascii-city-engine): address droid-review round 2 on PR #322 1. [P1] Engine: skip crossing props in the billboard loop so crosswalks render only as the documented ground band (no more floating '=' above each of the 168 crossings — a regression from the prior fix). 2. [P3] Engine: nearestStreet() filters to kind==='road' so the HUD 'On:' line names the street, not a named plaza/sidewalk ('Market Plaza' etc). 3. [P2] Validator: require each sign's text to be a recorded road name (collected from surfaces), enforcing the documented never-invented signage contract. A fabricated 'Made Up Avenue' sign now FAILs. 4. [P2][security] Validator: short-circuit the tile loop when MAX_FEATURES_PER_TILE is exceeded, and replace O(n^2) duplicate-id .count() scans with single-pass Counters, bounding the quadratic paths. Verified: dense pack 1411/0 exit 0; fabricated-sign repro flags only the injected sign and exits 1; broken/crash/dos/null packs all exit 1 with no tracebacks; JS node --check OK; validate-skills 154 green; blocklist clean. * fix(ascii-city-engine): address droid-review round 3 on PR #322 1. [P2] Validator: emit the signs rule unconditionally so a null/non-list 'signs' value FAILs instead of passing silently (was gated on a truthy list check). 2. [P2] Validator: validate sign text against a pack-wide road-name set gathered across all tiles, so a sign in one tile may name a road whose surface lives in another (the documented multi-tile case). 3. [P2] Schema: require non-empty id/kind/text (minLength 1) on props and signs so the schema and validator agree on empty-string rejection. 4. [P3] Engine: drop dead signGrid/IX.key (the sign overlay iterates world.signs directly); cap edge-rasterization steps so a degenerate resolution (0) or pathologically long edge cannot spin unboundedly. 5. [P3] Validator: unknown prop kinds now pass with a reported fallback-'?' note instead of hard-failing, matching the documented fallback glyph and the engine's behavior. Verified: dense pack 1411/0 exit 0; signs:null FAILs; fabricated sign FAILs; multi-tile sign-to-road reference PASSes; broken/crash/dos/null all exit 1; JS node --check OK; validate-skills 154 green; blocklist clean. * fix(ascii-city-engine): address droid-review round 4 on PR #322 1. [P2] Engine: render props as once-per-frame perspective-projected one-cell billboards in an overlay pass (like signs) instead of during the ray march, eliminating the multi-row vertical streak a close prop produced. Verified in a live browser: signals/trees/crosswalks now render as discrete single cells. 2. [P3][security] Engine: bound aggregate rasterization in buildIndices() — cap surfaces (5000) and cells per surface (40000) so a crafted pack cannot freeze the tab on load (the validator's caps are not applied client-side). 3. [P2][security] Engine: guard sign text (missing/non-string text now skips the sign instead of throwing in the rAF loop and freezing the view). 4. [P3] Validator: still collect building/surface IDs for oversized tiles so pack-wide uniqueness detection runs even when the per-feature geometry checks are short-circuited (duplicates in an over-cap tile are no longer hidden). Verified: dense pack 1411/0 exit 0; live browser render shows discrete props (no streaks); broken/crash/dos/null/nullsign/fabric all exit 1, valid multi-tile pack exit 0; JS node --check OK; validate-skills 154 green; blocklist clean. * fix(ascii-city-engine): address droid-review round 5 on PR #322 1. [P2][security] Engine: guard terrain() against non-finite x/y and guard the prop/sign overlay passes against non-array, non-object entries, so a malformed pack (missing y, signs=42, null entries) degrades gracefully instead of throwing in the rAF loop and freezing the view. Verified in a live browser: a pack with signs=42 + a prop missing y renders with the frame loop alive and no console errors beyond the favicon 404. 2. [P3] Engine: per-surface 'seen' set now dedupes cells across edges (was per-edge), eliminating the repeated linear includes() scan that made the rasterizer quadratic in the worst case. 3. [P3] Engine: raise the per-edge step cap to 20000 since the per-surface cell cap bounds total work, so long edges are fully sampled at the 2-5 m resolutions raleigh-poc.md recommends (fixes road-styling drops). 4. [P3] Validator: lower MAX_FEATURES_PER_TILE to 50,000 (shipped pack is 1,385), bounding the quadratic pair tests more tightly. 5. [P3] raleigh-poc: correct walkthrough step 3 — East Hargett sign is ~141 m behind the spawn, not ahead; only North Wilmington is ahead. HUD count guards signs/props as arrays. Verified: dense pack 1411/0 exit 0; broken/crash/dos/null/nullsign/fabric all exit 1, valid multi-tile pack exit 0; malformed-pack live render survives; JS node --check OK; validate-skills 154 green; blocklist clean. * fix(ascii-city-engine): address droid-review round 6 on PR #322 1. [P1] Engine: props/signs overlay passes now use the corrected perpendicular distance (d*cos(ray_angle-heading)) for row projection, distance scaling, and the depth test — matching the ray march — so FOV-edge objects project to the right row and no longer falsely occlude or poison later depth tests. 2. [P2][security] Engine: terrain() guards malformed terrain metadata (missing terrain/resolution, non-positive resolution, missing origin, null elevations), so a crafted pack degrades to a clean error instead of freezing the tab. Verified live: a resolution-0/null-elevations pack shows 'Cannot load...' with no page errors. 3. [P3] raleigh-poc: walkthrough step 3 corrected — W/S only translate, so a 167-deg-off sign needs A/D rotation, not 'hold S'. 4. [P3][security] Validator: all_points() guards buildings/surfaces/props/signs against truthy non-iterables (e.g. props=42), matching the other null guards, so malformed packs report structured FAIL instead of an uncaught TypeError. Verified: dense pack 1411/0 exit 0; crash/dos/null/nullsign/fabric/props42/ broken all exit 1 (no tracebacks); valid multi-tile exit 0; malformed-terrain live render shows clean error, no freeze; JS node --check OK; validate-skills 154 green; blocklist clean. * fix(ascii-city-engine): address droid-review round 7 on PR #322 1. [P2] Engine: spatial-index buildings (footprint bbox -> grid cells) so the render loop and collision test find nearby buildings in O(nearby) instead of scanning the whole O(buildings) list per ray sample. Browser-measured frame cost dropped ~62ms (16 FPS) to 12.5ms mean (~80 FPS) on the dense pack. 2. [P2][security] Engine: terrain() guards null/ragged elevation rows, so a pack with a null row degrades to a clean error instead of freezing the tab (live-verified: null-row pack shows 'Cannot load...', no page errors). 3. [P2][security] Engine: cap sign text at 80 chars in the overlay pass, so a pathological pack-supplied sign cannot drive an unbounded per-frame loop. 4. [P3] Validator: reference FALLBACK_GLYPH constant (was dead) in the unknown-kinds report message. 5. [P3] raleigh-poc: fix stale expected validator tail (was 30/25; actual is 159/899, rules_passed=1411). Verified: dense pack 1411/0 exit 0; crash/dos/null/nullsign/fabric/props42/ broken all exit 1, valid multi-tile exit 0; null-row pack shows clean error, no freeze; ~80 FPS browser-measured on dense pack; JS node --check OK; validate-skills 154 green; blocklist clean. * fix(ascii-city-engine): address droid-review round 8 on PR #322 1. [P1][security] Engine: bound the building spatial-index rasterization with MAX_BUILDING_PTS (2000) and MAX_BUILDING_CELLS (40000) and require >=3 finite footprint points, so a ~100-byte crafted footprint cannot drive a ~1e10- iteration synchronous hang on load (the surface rasterizer's cap, applied to the building index I added in round 7). 2. [P2][security] Engine: facingBuilding() filters footprints to finite points before reducing, so a building with a null element in its footprint no longer throws in the rAF loop on frame 1. 3. [P2][security] Engine: collides() and pointNearPolyline() filter footprints/ polylines to valid array points before edge tests, so null footprint points no longer throw once the player enters those cells. 4. [P3] Validator: correct the feature-cap comment to 'buildings + surfaces + props + signs combined' (signs were already counted). Verified: dense pack 1411/0 exit 0; ~85 FPS browser-measured (perf fix intact); badfoot pack (null-point + missing-footprint buildings) renders with zero page errors and frame loop alive; crash/dos/null/nullsign/fabric/props42/broken all exit 1, valid multi-tile exit 0; JS node --check OK; validate-skills 154 green; blocklist clean.
3.4 KiB
City Provider Contract
A city pack is a directory with manifest.json and one or more JSON world tiles. The engine reads only this contract; it must not branch on city name.
Manifest semantics
Validate manifest.json against ../templates/city-pack-manifest.schema.json.
name: stable lowercase pack identifier.version: pack release version.crs: local meter CRS description, including origin or EPSG code.bounds: inclusivemin_x,min_y,max_x,max_yin that CRS.tiles: non-empty relative paths contained by the pack directory.spawn: optional walkable local coordinate and heading.provenance: source records with public URL, license, ISO retrieval date, and optional confidence.
Paths must be relative, resolve inside the pack, and refer to regular JSON files. Manifest bounds must contain every tile's terrain, footprint, surface, and prop extent.
World-tile semantics
Validate each tile against ../templates/world.schema.json.
terrain: rectangular row-major elevations, meter resolution, local origin, and provenance. The extent is[origin_x, origin_x+(columns-1)*resolution]by[origin_y, origin_y+(rows-1)*resolution]. Null denotes a DEM void.buildings: globally unique IDs, simple closed-by-interpretation footprint polygons with at least three distinct vertices, meter base and positive height, stable color, and provenance/confidence.surfaces: IDs, kinds such as road/sidewalk/path/park/water, polyline or polygon geometry, and an explicitwalkableflag. Surfaces annotate and render the ground; they do not replace terrain height.props: lightweight point objects such as trees, lamps, signals, and signs. Each carriesid,kind,x,y, optionallabel, and provenance. The documented per-kind glyph map is:traffic_signal=T, street_lamp=i, tree=t, bus_stop=B, bench=b, bollard=o, fire_hydrant=f, crossing==. A kind with no mapping renders with the fallback glyph?and is flagged by the validator.signs(optional): street-name text billboards anchored at a real location, each carryingid,text,x,y, optionalanchor_way(the source road's way id), and provenance. Signtextmust equal anametag present in the source road data — never invented.
A provider may split content into tiles, but duplicate building and surface IDs are forbidden. A consumer may stream or spatially index tiles without changing semantics.
V1 vertical limitation
V1 models exactly one ground height per (x, y) column. It cannot represent a walkable bridge with walkable space beneath it, a tunnel under terrain, stacked interiors, or stairs between levels. Buildings are solid and non-enterable.
The top-level extensions.surface_graph name is reserved for a future graph of distinct walkable surfaces, portals, and vertical relationships. Producers may preserve source hints there, but v1 engines must ignore the extension and must not claim bridge/tunnel traversal. Do not overload the terrain array or surface records to fake multiple heights.
Validation behavior
Run:
python3 scripts/validate-city-pack.py path/to/pack
The validator uses only the Python standard library and network-free local files. It prints one PASS or FAIL line per rule, then summary counts. Exit 0 means every rule passed; any schema, geometry, extent, provenance, or uniqueness failure exits 1. The fixture at ../assets/deliberately-broken-pack/ intentionally demonstrates failures and is not a usable provider.