Verified G's stock_godverse/3962749/ (120 real records, Monster Robot Party crate 550, 2 x 2048^2, cell 256) in a fresh context — deliberately BEFORE the merge, so a contract failure would block it. It didn't. PASS — the contract holds; G re-emits nothing: - Resolves through the existing stockAdapter path via a base override (no C code change for the single-shop alpha). 120 items / 2 atlases / 8-of-8 stock meshes atlas-backed (real covers: peperecordings, DEEPCHILD, "39", mff FREAKS...). - Ceilings hold: 118 draws with real covers (<=350 law), pathOK, 0 carves. - Fail-soft holds (clean cache): missing atlas -> null -> parody (20 meshes, 110 draws); no adapter (?noassets shape) -> parody, zero fetch. 0 console errors. - G's index compliant: items[] sorted by id (my determinism ask honoured), provenance, uv in [0,1], 2 atlases = exactly the cap, cell_w/h harmless (C reads uv, never cell). CORRECTED MY OWN CEILING (LANE_C_PUB §7.2): I wrote "<=8 MB decoded" from an assumed ~16-item "typical shop". A real record shop is 120 records => 2 x 2048^2 ~= 32 MB, 4x my number. The structural rules held (<=2 atlases; 2048^2 for 60+ items — G landed on the cap); only the MB figure was wrong, because it described a shop that doesn't exist. New: <=2 atlases @<=2048^2, LRU = 1 shop's atlas set resident => <=32 MB peak (bounded; vs E's 181 MB unbounded today). Do NOT shrink the cell below 256 to save VRAM — the dig renders covers close up. G's ask #1 CONFIRMED and worse than reported (LANE_C_PUB §8 — C's spec, C's fix): stockpack keys _packs/_resolved by type alone, so base is ignored on a cache hit (measured: the 2nd preload returned the identical object). (1) two stocked shops collide; (2) it BREAKS FAIL-SOFT — an unstocked shop of a cached type inherits that crate instead of parody. Seam: key by type+base, per-shop preload on entry, LRU=1 + dispose; C consumes whatever identity field lands, C does not invent it. Two gaps block F's #7a, neither C's: the crate has no street (redhill_real absent; monsterrobot not in any shops[] — G's ask #2 to E/Fable), and interior_mode.js:192 pins base to assets/models/ (needs §8 + the identity field). Committed from a temp git worktree on main — the shared checkout stayed on lane-G-godverse (Fable's review) and G's branch keeps its disjoint namespace. Docs-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
325 lines
24 KiB
Markdown
325 lines
24 KiB
Markdown
# LANE C — interiors contract (venues + real-town class mapping) → Lanes A/D/E/F
|
||
|
||
> **v3.0-FROZEN** · 2026-07-16 · durable contract for Lane D + Lane F. §0–§5 describe the *shipped*
|
||
> venue state; changes require a version bump (CITY_SPEC amendment law). Verified R13 (build) + R14 (audit).
|
||
> **v3.1 amendment:** §3 became the `stage.backline[]` contract (ledger #5 — the amp unification).
|
||
> **v4.0-beta amendment (2026-07-16):** **§6 added — the real-town shop-class → archetype mapping**
|
||
> (ledger #2, the density widening; C→E handshake). The frozen venue contract (§0–§5) is unchanged.
|
||
> **v4.0 amendment (2026-07-16):** **§7 added — the G2a per-shop stock-atlas contract** (C's interior
|
||
> draw/tex ceilings, answering Lane G + E's review). §0–§6 unchanged.
|
||
> **v5.0-alpha amendment (2026-07-16):** **§7.2's VRAM number corrected + §7.8 added** — G's first real
|
||
> crate (120 records) proved my "≤8 MB" figure was derived from a wrong assumption, and the per-shop cache
|
||
> seam is now specified for G+F. **G's R23 atlas needs no re-emit.** §0–§6 unchanged.
|
||
|
||
*The district ships **three** venue archetypes behind `?gigs=1` — `pub`, `band_room`, `rsl`. Lane A
|
||
converts a chosen shop to a venue kind in place (`shop.type = kind`); C keys the interior recipe off
|
||
`shop.type`. All three return the SAME shape (below), so Lane D drops rigs in and Lane F reads state with
|
||
no per-kind branching. Non-venue rooms are byte-identical (venue code runs only for `recipe.venue`, on its
|
||
own seed sub-streams). Verified fresh (seeds 20261990/1990/7/42/101 + 90-build soak): 0 throws · 0 carves ·
|
||
0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst build 8.3 ms. Budgets in §3.*
|
||
|
||
---
|
||
|
||
## 0. The pose `ry` convention (pinned) — READ THIS FIRST
|
||
|
||
**Canonical convention (every C pose uses it):** a pose's `ry` is the yaw of a thing whose **front /
|
||
outward normal is local −Z at ry=0**, i.e. world-facing `(−sin ry, −cos ry)`. `counter.stand`,
|
||
`browsePoints`, `watchPoints` and `bandPoses` ALL follow it, and the box placeholder (`placeholder.js`) is
|
||
built to it (toes at −Z). `keepers.js` greet math assumes it. **Drop a rig in at a pose's `ry` and it faces
|
||
the right way — no per-consumer flip.**
|
||
|
||
**Why no flip (the R13 fix, for the record):** the GLB fleet meshes ship facing local **+Z** at ry=0 — the
|
||
opposite of the −Z convention (verified: the anterior toe-bone vector `toe − foot` reads `(≈0, +0.99)`).
|
||
Before R13 this was compensated per-consumer, inconsistently: `band.js` flipped by π (band/crowd correct,
|
||
placeholders backward) while `keepers.js` did not (placeholders correct, **keepers + browsers 180° backward
|
||
whenever the GLB fleet was loaded** — they'd been shipping back-to-front). **D fixed it at the source in
|
||
`rigs.js buildFigure`: the cloned `inner` is rotated π about Y so every GLB rig faces −Z**, matching the
|
||
placeholder and every C pose. All consumer flips were then deleted. Net: keepers, browsers, band and crowd
|
||
all face correctly, GLB and placeholder modes are consistent, and C's pure-data poses never moved (no golden
|
||
touched). If you add a new rig consumer, plant it at the pose `ry` with no flip.
|
||
|
||
---
|
||
|
||
## 1. What `buildInterior` returns for a venue (`shop.type` ∈ `pub` | `band_room` | `rsl`)
|
||
|
||
Everything below is **pure data on the return** (room-local coords, the ry convention in §0). Lane D drops
|
||
rigs straight in; Lane F reads state — no new lifecycle vs. the R9 keeper/browser seam.
|
||
|
||
### `room.stage` — the band stage, a **4-piece** (null for non-venues)
|
||
```js
|
||
room.stage = {
|
||
x, z, // deck centre (against the back wall)
|
||
w, d, // deck size (m) — varies by kind (table below)
|
||
deckY, // deck top surface height (m) — the front line stands here
|
||
riserY, // drum riser top height (m) = deckY + 0.16 — the drummer sits here
|
||
frontZ, // z of the deck lip (the crowd stands in front of this, +Z)
|
||
bandPoses: [ { x, z, ry, role, y, seated? } ×4 ],
|
||
}
|
||
```
|
||
- `bandPoses[0..2]` = the **front line**: `role` ∈ `guitar` / `vocal` / `bass`, across the deck lip, `y = deckY`.
|
||
- `bandPoses[3]` = the **drummer**: `role: 'drums'`, `seated: true`, up-stage centre on the drum riser,
|
||
`y = riserY` (= `deckY + 0.16`, the `fittings.js` stage riser box).
|
||
- **Lane D:** lift each member to its own `pose.y` (front line = `deckY`, drummer = `riserY`). `role` hints
|
||
the instrument — E's GLBs via `opts.instrumentFor(role)` (electric_guitar/bass_guitar/mic_stand/drum_kit),
|
||
primitives otherwise (asset law). `seated:true` → seated bob. `ry = π` already faces the audience (+Z) — no
|
||
flip (§0).
|
||
- The band is a **4-piece** for v3.0 (front-line trio + drummer). Reverting to a trio would be a version bump
|
||
(drop `bandPoses[3]`), not a runtime toggle.
|
||
|
||
### `room.watchPoints` — the audience, **cap by kind** (empty `[]` for non-venues)
|
||
```js
|
||
room.watchPoints = [ { x, z, ry, dance, slotIndex } ] // seeded floor poses, all facing the stage centre
|
||
```
|
||
- **Crowd cap by kind: `pub` 8 · `band_room` 8 · `rsl` 12** (the RSL is the crowd-cap stress case). The room
|
||
fills what geometry allows up to the cap; **the count IS the crowd cap** (F asserts crowd ≤ `watchPoints.length`).
|
||
- `dance` seeded **~⅓ true** ("a bit of both"). D: `dance:false` → stand-and-watch idle; `dance:true` → bob/sway
|
||
with a **seeded phase offset** (use `slotIndex`/coords). Every point walkable, reachable, clear of the keeper
|
||
stand + browse points + stage/PA. Deterministic per `shop.seed`.
|
||
|
||
### `room.counter` / keeper — unchanged
|
||
The bar is a normal counter; the barkeep is just a keeper at `room.counter.stand` (R9 seam). Faces the
|
||
customer correctly under §0.
|
||
|
||
### `room.audio.gigKey` — the live bed
|
||
`room.audio = { musicKey, toneKey, gigKey? }`. The manifest key **is** the gigKey, canonical form
|
||
**`gig-<genreKey>`** — no mapping table. F passes gig state via `opts.gig`:
|
||
```js
|
||
buildInterior(rslShop, THREE, { gig: { on: true } }) // → room.audio.gigKey === 'gig-covers'
|
||
```
|
||
- Genre resolves `opts.gig.genreKey` → the venue's own `shop.genreKey` (A sets it) → a kind default
|
||
(`pub`→pubrock, `band_room`→grunge, `rsl`→covers). Present **only when a gig is on**.
|
||
- **Prefer `gigKey` over `musicKey`** while the gig plays. E's beds are `gig-pubrock` / `gig-grunge` /
|
||
`gig-covers`; a missing key → silence (audio law), band still visible.
|
||
- C hand-builds `'gig-'+genreKey` (byte-identical to citygen's `gigKeyFor`, selfcheck-asserted) rather than
|
||
importing it — deliberately, to keep the interiors lib standalone (it never imports citygen).
|
||
|
||
### The venue table (C-owned tuning)
|
||
| kind | archetype bias | ceiling | deck `w × d`, `deckY` | crowd cap | genre → gigKey | dressing |
|
||
|---|---|---|---|---|---|---|
|
||
| `pub` | wide / hall | 3.6 m | `0.58·W × 2.0`, 0.32 | 8 | pubrock → `gig-pubrock` | pub tables, sticky carpet, bar |
|
||
| `band_room` | cosy / pokey / wide | **3.4 m (low)** | `0.62·W × 1.6`, **0.20** (barely a riser) | 8 | grunge → `gig-grunge` | near-empty standing room, tin-shed dingy |
|
||
| `rsl` | hall / wide | **4.0 m (tall)** | `0.50·W × 2.4`, **0.40** | **12** | covers → `gig-covers` | flock walls, club carpet, bistro tables, members' bar |
|
||
|
||
## 2. Quiet-night variant
|
||
No `opts.gig` → a normal interior: `stage` + `watchPoints` are still returned as data, but there's **no
|
||
`gigKey`** (room-tone + maybe the seeded radio `musicKey`), and D spawns no crowd. A seeded dark night reads
|
||
true — the venue is just a quiet pub/club.
|
||
|
||
## 3. `stage.backline[]` — the amp slots (v3.1, ledger #5)
|
||
|
||
`room.stage.backline = [ { x, z, ry, y } ×2 ]` — seeded up-stage amp/spare-cab poses, **pure data** (C plants
|
||
no amp meshes as of v3.1). This is the unified backline: **Lane D plants E's `guitar_amp` GLB here** (its own
|
||
primitive fallback under `?noassets`) — one amp system, GLB when present. C's old primitive ampStacks are
|
||
**dropped** from all three venue recipes.
|
||
- **slot[0]** = stage-right (the primary amp) at `(stage.x + stage.w·0.36, deckY, stage.z − stage.d·0.22)` —
|
||
identical to D's R15-verified plant, so wiring D's amp to `backline[0]` moves nothing on screen.
|
||
**slot[1]** = stage-left spare cab (mirror). `ry = π` faces the audience (+Z, the §0 convention); `y = deckY`.
|
||
- **Guarantees** (verified fresh, seed 20261990, pub/band_room/rsl): every slot is **up-stage of the front
|
||
line**, flanks the drum riser, **clear of all 4 bandPoses** (nearest-bass clearance **0.87 m pub / 0.70 m
|
||
band_room / 0.92 m rsl**) and **all watchPoints** (≥1.95 m), and **on-deck** (within the deck footprint).
|
||
Deterministic per `shop.seed`. **Lane D:** verify no interpenetration once wired (C's R15 clearance discipline).
|
||
- **Draw impact:** dropping C's primitives shaved ~4 draws off each empty venue shell (drawSweep glb-off:
|
||
pub 122 · band_room 120 · rsl 131 ≤350); D's GLB/primitive at `backline[0]` adds it back on the gig night.
|
||
- *v3.2 (parked):* the drummer sit clip, instrument LOD, and C's `audioEmitter` idea.
|
||
|
||
## 4. Budgets (measured R14, seed 20261990, fresh context)
|
||
- **Empty shells** (`drawSweep` glb-off): band_room **124** · pub **126** · rsl **135** — town-wide worst
|
||
161 (opshop/hall). ≤ 350. ✓ (identical to R13 — C geometry unchanged).
|
||
- **Gig-night interior draws**, D's `GigCrew` with the real instrument GLBs + amp (localdepot): pub **60** ·
|
||
band_room **52** · rsl **71**. Asset-free (placeholder crowd, no GLBs) worst RSL **237** (16 rigs). ≤ 350. ✓
|
||
- **Tris**: fleet peds ~2.7k each; E's instrument GLBs **14k each** (decimated to the ≤15k target), 5 on a
|
||
stage = 70k — the instruments dominate. RSL gig-night ≈ **182k tris** once the async GLBs settle. The
|
||
interior gate is **draws** (passes); tris are the *street* budget (200k), so this is not an interior gate.
|
||
*Note for E/F:* F's gig-night smoke reports ~34k because it measures **before** the async instrument GLBs
|
||
load; the settled scene is ~180k. A camera-distance LOD on instruments is a v3.1 candidate.
|
||
- **Determinism / release-law**: C's interior code is unchanged since R13; D's continuity + RY-fix ride
|
||
disjoint rng namespaces (`ctx.stream(salt)` vs `rng(citySeed,kind,id)`) and cannot perturb C's placement
|
||
or any golden. 90-build venue soak: 0 determinism · 0 carve · 0 path-fail · worst 8.3 ms; leak geo0/tex0.
|
||
|
||
## 5. The archetypes (C-owned)
|
||
`band_room` = a tin shed: small (cosy/pokey), low ceiling, barely-a-riser deck, near-empty standing floor,
|
||
bar in the corner — the PA louder than the room. `rsl` = carpet-and-flock club: the biggest floor in town
|
||
(hall/wide), a proper raised stage at one end, bistro tables round the edge, members' bar, the biggest crowd
|
||
cap. Both reuse the `pub` venue machinery (`theme.js venueSpec` + `layout.js` placeStage/placeWatchPoints);
|
||
only the dressing + `venueSpec` differ. Stage/PA keep GLB slots for Lane E (primitive fallback holds); the
|
||
backline amp is D's, planted at `stage.backline[0]` (see §3).
|
||
|
||
---
|
||
|
||
## 6. Real-town shop-class → interior archetype mapping (v4.0-beta, ledger #2) → Lane E
|
||
|
||
**Directive (John, kickoff — SUBTLE: texture, not takeover).** The secondhand shops stay the **heroes** of
|
||
the town (they *are* the game). The widening adds general retail as *texture* — the everyday main-street
|
||
staples a real Australian town has — mapped onto **existing** archetypes (**no new archetypes this round**).
|
||
The player should feel the town's more alive, not that the game changed.
|
||
|
||
**How it's applied (E owns the emission).** `plan_osm` keeps only **registry types** (`SHOP_TYPES` — unknowns
|
||
flatten to `opshop`). So E maps each fetched OSM `shop=*` / `amenity=*` value to a **registry type** below and
|
||
bakes it into the cache's `shop.type`. Every target is an existing type, so C's recipes, A's registry, and the
|
||
budgets are all unchanged. Unknown/unlisted classes degrade safely to `opshop` (R18-proven: 18 real OSM types
|
||
all built valid).
|
||
|
||
**Heroes — secondhand (unchanged; NEVER subsampled):**
|
||
| OSM class | → type |
|
||
|---|---|
|
||
| `charity`, `second_hand`, `antiques`, `vintage` | `opshop` |
|
||
| `books`, `bookshop` | `book` |
|
||
| `music` | `record` |
|
||
| `video`, `games` | `video` |
|
||
| `pawnbroker` | `pawn` |
|
||
| `toys` | `toy` |
|
||
|
||
**Widened — everyday main-street texture (general retail):**
|
||
| OSM class(es) | → type | wears it as |
|
||
|---|---|---|
|
||
| `bakery`, `cafe`†, `confectionery`, `pastry`, `chocolate`, `deli`/`delicatessen`, `butcher`, `cheese`, `coffee`, `tea` | `milkbar` | the food counter |
|
||
| `convenience`, `kiosk`, `general`, `dairy` | `milkbar` | the corner store |
|
||
| `newsagent`, `stationery` | `milkbar` | papers + magazines |
|
||
| `chemist`, `pharmacy`, `cosmetics`, `perfumery` | `milkbar` | dispensary counter |
|
||
| `clothes`, `fashion`, `boutique`, `shoes`, `bag`, `jewelry`/`jewellery` | `dept` | the dressed retail floor |
|
||
| `department_store`, `variety_store`, `supermarket` | `dept` | big general retail |
|
||
| `hardware`, `doityourself`, `trade`, `electronics`, `mobile_phone`, `furniture`, `houseware`/`homeware` | `dept` | shelved goods |
|
||
| `greengrocer`, `florist`, `farm`, `garden_centre` | `stall` | produce/plants on trestles |
|
||
|
||
† `cafe`/`fast_food` are `amenity=*`, not `shop=*` — fetch them from the amenity tag; everything else is `shop=*`.
|
||
**Fallback:** any fetched class not listed → `opshop`.
|
||
|
||
**Copy-paste map for E (`build_towns.py`):**
|
||
```json
|
||
{
|
||
"charity":"opshop","second_hand":"opshop","antiques":"opshop","vintage":"opshop",
|
||
"books":"book","bookshop":"book","music":"record","video":"video","games":"video",
|
||
"pawnbroker":"pawn","toys":"toy",
|
||
"bakery":"milkbar","cafe":"milkbar","confectionery":"milkbar","pastry":"milkbar","chocolate":"milkbar",
|
||
"deli":"milkbar","delicatessen":"milkbar","butcher":"milkbar","cheese":"milkbar","coffee":"milkbar","tea":"milkbar",
|
||
"convenience":"milkbar","kiosk":"milkbar","general":"milkbar","dairy":"milkbar",
|
||
"newsagent":"milkbar","stationery":"milkbar",
|
||
"chemist":"milkbar","pharmacy":"milkbar","cosmetics":"milkbar","perfumery":"milkbar",
|
||
"clothes":"dept","fashion":"dept","boutique":"dept","shoes":"dept","bag":"dept","jewelry":"dept","jewellery":"dept",
|
||
"department_store":"dept","variety_store":"dept","supermarket":"dept",
|
||
"hardware":"dept","doityourself":"dept","trade":"dept","electronics":"dept","mobile_phone":"dept",
|
||
"furniture":"dept","houseware":"dept","homeware":"dept",
|
||
"greengrocer":"stall","florist":"stall","farm":"stall","garden_centre":"stall"
|
||
}
|
||
```
|
||
Default (not in the map) → `opshop`.
|
||
|
||
**Subtlety guardrails (E):**
|
||
- Fetch only these tasteful staples — **not** every OSM `shop=*`. Skip vehicle/tyres/fuel, funeral, storage,
|
||
industrial, and pure services the player never browses (`hairdresser`, `beauty`, `travel_agency`, `bank`,
|
||
`laundry`, `estate_agent`).
|
||
- **Never** subsample the secondhand heroes.
|
||
- If a town's general-retail count would visually **drown** the secondhand strip, seed-subsample the *texture*
|
||
classes (the milkbar/dept/stall targets) toward **~2–3× the secondhand count**, and count the drops in
|
||
`norm.dropped` like every other drop.
|
||
|
||
**Why these targets:** `milkbar` (counter + fridge + magazine rack + glass case) is the canonical Australian
|
||
counter shop — it wears cafe/bakery/deli/newsagent/chemist naturally; `dept` (cube shelves + clothes racks +
|
||
glass cases) wears clothes + hardware + big retail; `stall` (trestles + crates) wears produce + florist. Three
|
||
general-retail looks + the six secondhand heroes + the three venues = variety with no new archetypes.
|
||
**Pre-verified (seed 20261990, real-town lot 8.5×14):** milkbar/dept/stall/opshop all build valid (0 carves,
|
||
≤143 draws); `dept` reads as a clean **dressed-retail floor** (cube shelves + clothes racks + counter, the
|
||
escalator does *not* dominate the ~7.9×11.7 room), so `clothes`/`hardware` → `dept` is confirmed. C re-checks
|
||
the full result in the five-town door-soak (ledger #6) once E's caches land. (FYI: C's `theme.js canonicalType`/ALIAS already recognizes several
|
||
of these — `deli`/`corner_store`→milkbar etc. — for any direct `buildInterior(rawClass)`; the authoritative
|
||
mapping for the caches is this table.)
|
||
|
||
---
|
||
|
||
## 7. G2a — per-shop stock atlases: C's interior ceilings (v4.0, ledger #3) → Lane G + Lane E
|
||
|
||
*Answering Fable's three priors + E's half (LANE_E_NOTES). **Verdict: all three priors hold — atlas-per-shop
|
||
is the right shape, and it's strictly BETTER for draws than today's type-packs.** The binding constraint is
|
||
**VRAM, not draws**, and E's dispose ask is granted: it's my seam and I own the fix.*
|
||
|
||
**1. Draw ceiling — ONE atlas per shop (hard cap 2).**
|
||
Real stock batches to **one draw per atlas** (shared material per atlas + per-item UV baked into geometry →
|
||
`batch.js` merges). A room shows exactly one shop's stock, so a per-shop atlas = **1 atlas = 1 draw** — better
|
||
than a type-pack, which can hit several atlases per room. **Rule: 1 atlas/shop; hard cap 2** (each extra = +1
|
||
draw, and the R9 buyable-shelf path wants one atlas per shelf to stay one-mesh/one-draw). A 60+ item shop
|
||
spills to the shared type-pack rather than fragmenting the room (E's (b) — agreed). *Headroom: `?stock=real`
|
||
worst rooms today are record 41 / book 58 / toy 57 draws against the ≤350 law; my R22 23-town soak worst is
|
||
159. Draws are not the constraint.*
|
||
|
||
**2. Texture/VRAM ceiling — the real limit. E's dispose ask: GRANTED, and it's mine to land.**
|
||
E's audit is correct and the cause is a deliberate C design note (`stockpack.js:11`): atlas textures are
|
||
"shared/cached in the pack (**NOT per-room**), like depot GLBs". That is **right for 3 bounded type-packs**
|
||
and **wrong for unbounded per-shop atlases** (23 towns × N shops). The ceiling:
|
||
|
||
> **⚠️ CORRECTED v5.0-alpha — measurement beat my assumption.** I wrote "≤8 MB decoded" by assuming a
|
||
> "typical shop" is ~16 items @1024². **G's first real crate is 120 records** (Monster Robot Party, crate
|
||
> 550) — a real record shop is nothing like 16 items. At the contract's own 256² cell that's 2 × 2048²
|
||
> = **~32 MB decoded**, 4× my stated figure. The *structural* rules were right (≤2 atlases; 2048² for a
|
||
> 60+ item shop) — only the MB number was wrong, because it was derived from a shop that doesn't exist.
|
||
> **G's R23 atlas is compliant; no re-emit.** The corrected ceiling:
|
||
|
||
- **Per shop: ≤2 atlases, ≤2048² each** (unchanged — G's 120-item crate lands exactly on the cap).
|
||
- **Resident: ONE shop's atlas set** (LRU = 1 shop, not 2) ⇒ **≤32 MB decoded peak**. Still bounded, and
|
||
still far better than the **181 MB unbounded** accretion E measured today.
|
||
- **≤1024² whenever the crate fits it** (≤64 items at 256²) — don't hand a 6-item shop a 2048².
|
||
- Cell size is E/G's call: **C reads `uv`, never `cell`** (so 256², 176×256 portrait, etc. are all fine).
|
||
Do **not** shrink the cell below 256² to save VRAM — the dig renders a cover close-up; resolution is the
|
||
one thing the player actually sees.
|
||
- **C lands the LRU cap + dispose in `stockpack.js`** (evicted shop's atlas texture + material disposed).
|
||
Type-packs keep today's shared cache (bounded, reused across rooms of a type) — unchanged.
|
||
- **Scheduling:** today's 181 MB accretion is **pre-existing**, and `?stock=real` is **opt-in**
|
||
(`index.html:196` — `params.get('stock')==='real' && !NOASSETS`), so it cannot touch the default boot and is
|
||
**not a v4.0-tag blocker**. I land the cap as **C's first v4.x item — before G2a ships**. G: build to the
|
||
ceiling; the loader will honour it.
|
||
|
||
**3. Index schema — E's (c) confirmed VERBATIM, keyed per shop.**
|
||
`stock_shop_<godverseShopId>_index.json` = `{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,
|
||
price_band,atlas,uv:[u0,v0,u1,v1]}]}`. `stockpack.js` consumes it unchanged (it already groups items by atlas
|
||
and shares a material per atlas). **UV origin stays top-left (image-natural) — C flips V for WebGL** (R7 law,
|
||
unchanged). Book cell 176×256 portrait is fine: **C reads `uv`, never `cell`**.
|
||
|
||
**4. Fail-soft + `?noassets` — unchanged law, confirmed.**
|
||
Index/atlas missing → `getStockPack` → null → **parody canvas + one warn** (R7, proven since). `?noassets`
|
||
fetches zero. A godverse town under `?noassets` stays silent-and-fine. `?stock=real` opt-in ⇒ G2a cannot
|
||
regress the default or classic boots.
|
||
|
||
**5. Determinism — one ask back to G.**
|
||
The seeded pick per bin is C's and stays: same `shop.seed` → same crate contents, byte-identical revisits.
|
||
That indexes into `items[]`, so **G must emit `items[]` in a deterministic order** (sort by `id`) — otherwise
|
||
a re-emit silently reshuffles every crate. Same discipline as E's byte-identical cache re-runs.
|
||
|
||
**6. Buy-anywhere (R9) — compatible, no change.**
|
||
`buildBuyableShelf` merges per-item-addressable covers from ONE atlas → one draw (`noBatch`). A per-shop atlas
|
||
satisfies that naturally (better than the type-pack's seeded `atlasPool` pick). Parody = not buyable stays.
|
||
|
||
**7. Parody law — E's (d), unchanged and not C's to relax.** Metadata deterministically parody-transformed;
|
||
cover art real; prices real-or-seeded. *(v5.0-alpha: John amended this at tier 1 — real titles/artists ship
|
||
in-house-green, flagged before public. Recorded in G's provenance; not C's to relax either way.)*
|
||
|
||
**8. The per-shop cache seam (v5.0-alpha — C's spec; G+F build to this). CONFIRMED BROKEN in R23.**
|
||
G's ask #1 is real and I reproduced it. `stockpack.js` keys `_packs`/`_resolved` by **`type` alone**, and
|
||
`preloadStockPack(type, {base})` **ignores `base` on a cache hit**. Measured (R23, fresh context):
|
||
- `preloadStockPack('record', {base:'assets/stock_godverse/3962749/'})` → the 120-record godverse pack ✓
|
||
- then `preloadStockPack('record', {base:'assets/models/'})` → **returned the identical object** ✗
|
||
|
||
Two consequences, the second worse than G's report:
|
||
1. **Two stocked shops collide** — the 2nd silently serves the 1st shop's crate (blocks v5.0-beta).
|
||
2. **It breaks the fail-soft law** — an *unstocked* shop of a cached type inherits that crate instead of
|
||
falling back to parody. (On a clean cache fail-soft is intact: missing atlas → `null` → parody, 0 errors.
|
||
The collision is what defeats it, so this is a correctness bug, not just a mix-up.)
|
||
|
||
**The seam (C owns it, C lands it):**
|
||
- **Cache key = `type + base`** (equivalently a `packKey`; a godverse shop's key is its `stock_godverse/<godverse_id>/` base). `getStockPack` takes the same key.
|
||
- **Resolution order** stays: explicit per-shop base → the type-pack base → `null` → parody.
|
||
- **Per-shop preload on entry** (interior build/enter), **not** a boot-time preload of every shop.
|
||
- **LRU = 1 shop's atlas set, disposed on eviction** (§7.2) — the load-bearing half of the §7 promise.
|
||
- **Identity link:** a stocked shop must carry its pack pointer on the shop record (G's `godverse_id` →
|
||
`stock_godverse/<id>/`). Not in the town cache today — G flagged it ("monsterrobot must be added to the
|
||
godverse cache's `shops[]` as id 3962749"). C consumes whatever field lands; **C does not invent it.**
|
||
|
||
**9. R23 verification — the first real crate, in a room (ledger #3). PASS.**
|
||
Measured against G's `3962749` atlas (120 real records, 2 × 2048², cell 256²) in a fresh context:
|
||
- **Resolves through the existing `stockAdapter` path** via a base override — no C code change needed for
|
||
the single-shop alpha. 120 items, 2 atlases, **8/8 stock meshes atlas-backed** (real covers, not parody).
|
||
- **Ceilings hold:** **118 draws** with real covers (≤350 law); room `pathOK`, 0 carves.
|
||
- **Fail-soft holds** (clean cache): missing atlas → `null` → parody (20 parody meshes, 110 draws, pathOK);
|
||
no `stockAdapter` (the `?noassets` shape) → parody, zero fetch. **0 console errors** throughout.
|
||
- **G's index is contract-compliant:** `items[]` sorted by id (my determinism ask honoured ✓), provenance
|
||
present ✓, `uv ⊂ [0,1]` ✓, 2 atlases = the cap ✓, `cell_w/cell_h` harmless (C reads `uv`) ✓.
|
||
- **Verdict: the contract holds. G re-emits nothing.** The only C-side work is §8's seam.
|