Lane E R40 §40.1+§40.4: the gate now watches every door the glass could walk through
§40.1 — the R39 blind spot, closed with both arms fired. Before: check_transmission()
parsed pipeline/_normalized/<file> only and returned SILENTLY when absent — so a
depot-only transmissive GLB (the longbench/streetlight shape) or a stale glass copy in
web/assets/models/ (what ?localdepot=1 actually serves) shipped unseen. After: all four
surfaces scanned — _normalized/ always, web/assets/models/ always, the DEPOT copy via
ranged GET of the GLB JSON chunk whenever no local file exists (206-aware, slices the
tailnet's 200-full), plus a sweep of every non-manifest .glb in both dirs (ped rigs and
dance clips load by literal path). A depot fetch failure is a LOUD warn, never a skip.
Controls demonstrated, not asserted:
arm 1 (served path): glass box_crate planted in web/assets/models/ only →
R39 gate rc 0 (the blind spot, live) · R40 gate rc 1 · fixture removed → rc 0
arm 2 (depot path): both local copies held aside, local mock depot serving glass →
rc 1 (depot copy) · clean bytes → rc 0 · REAL tailnet depot, local absent →
rc 0 / 0 warnings (production fetch path proven)
Nothing was ever published to the real depot namespace; sha1 6c327d86 restored both
copies; final validator on the real tree rc 0, 0 errors, 0 warnings, 39 manifest GLBs
+ 40 swept.
§40.4 — the arcade is a PLACEMENT rule, not an asset. Measured: one 42.00 × 5m arcade
edge, posts at d/2+2.2-0.2 = ±0.50m from the centreline — two rows 1.00m apart, 34
colliderless posts, 0.60m open slot. The kit designed as one thing: the roof spans
(depth 2.5 = half-lane, slabs meet flush) and a spanned roof has no posts. Proved
without touching B's tree — rule applied in-flight by a rewriting no-store server,
shot from inside the lane: draws 124→124 (+0), tris unchanged. Rule handed to B with
file:line; B applied it this round.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3e1ff312a5
commit
cf6ac64d75
@ -7,7 +7,88 @@
|
||||
> lanes need from this one, newest first) and **[`pipeline/AUDIT.md`](pipeline/AUDIT.md)**
|
||||
> (provenance, licence zone, measurements and method, per round). Recorded in R39 rather than
|
||||
> quietly resurrected: a progress file whose last entry is 27 rounds old is a trap for the next
|
||||
> reader, and the honest fix is a signpost, not a backfill.
|
||||
> reader, and the honest fix is a signpost, not a backfill. *(One exception below: the R40 tasking
|
||||
> explicitly asked for its control-arm transcripts in THIS file, so §40 lives here AND is
|
||||
> cross-referenced from the notes.)*
|
||||
|
||||
## Round 40 (§40.1+40.4) · 2026-08-04 — *(entry here by explicit R40 tasking; cross-lane story in `docs/LANES/LANE_E_NOTES.md` §40)*
|
||||
|
||||
### §40.1 — transmission gate blind spot CLOSED, both control arms demonstrated
|
||||
|
||||
`pipeline/validate_manifest.py` `check_transmission()` now covers every path the game can load a
|
||||
GLB from: `pipeline/_normalized/` (as before), **`web/assets/models/`** (the `?localdepot=1` served
|
||||
mirror — never scanned before), **the depot copy** for any manifest GLB with no local file in either
|
||||
dir (ranged GET of the GLB JSON chunk; 206-aware, slices a 200 — the tailnet depot returns 200-full),
|
||||
and a **sweep of every non-manifest `.glb`** in both dirs (ped rigs, dance clips — `rigs.js` loads
|
||||
them by literal path). A depot fetch failure is a loud WARN (`depot copy UNSCANNED`), never a skip.
|
||||
|
||||
**Fixture:** `procity_fit_box_crate_01.glb` with `transmissionFactor: 1` injected into material[0]
|
||||
(the exact defect `strip_transmission.py` removes; injector kept in the session scratchpad, BIN
|
||||
untouched). Clean sha1 `6c327d8624bc90203275a341128abb546f5d69e0` (staging = served, verified after
|
||||
cleanup). **Depot arm used a LOCAL MOCK depot (`python3 -m http.server` serving `a/<file>`) — no
|
||||
fixture ever touched the real depot namespace.**
|
||||
|
||||
**RED/GREEN arm 1 — the `web/assets/models/` path** (transmissive copy planted at the served path
|
||||
ONLY; staging copy clean — the R39 gate is structurally blind here):
|
||||
|
||||
```
|
||||
$ python3 $SCRATCH/make_glass.py pipeline/_normalized/procity_fit_box_crate_01.glb \
|
||||
web/assets/models/procity_fit_box_crate_01.glb
|
||||
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/.validate_r39_ab.py # R39 copy of the gate
|
||||
OK — 0 errors, 0 warning(s) # old rc=0 ← the blind spot, live
|
||||
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py # R40 gate, same tree
|
||||
ERR procity_fit_box_crate_01.glb (assets/models copy): material[0] 'Material.001' has
|
||||
transmissionFactor 1 — three.js will run a transmission PRE-PASS ... # new rc=1 ← RED
|
||||
$ cp $SCRATCH/hold/procity_fit_box_crate_01.glb web/assets/models/ # remove fixture
|
||||
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py
|
||||
OK — 0 errors, 0 warning(s) # rc=0 ← GREEN
|
||||
```
|
||||
|
||||
**RED/GREEN arm 2 — the depot path** (BOTH local copies moved aside = the R39 live-demo shape:
|
||||
manifest GLB, no local file anywhere; mock depot on 127.0.0.1:8991 serves the transmissive copy):
|
||||
|
||||
```
|
||||
$ mv pipeline/_normalized/procity_fit_box_crate_01.glb $SCRATCH/hold/norm_...
|
||||
$ mv web/assets/models/procity_fit_box_crate_01.glb $SCRATCH/hold/models_...
|
||||
$ GOD3_DEPOT=http://127.0.0.1:8991 python3 pipeline/validate_manifest.py
|
||||
ERR procity_fit_box_crate_01.glb (depot copy): material[0] 'Material.001' has
|
||||
transmissionFactor 1 — ... # rc=1 ← RED
|
||||
# mock now serves the CLEAN bytes (local still absent):
|
||||
$ GOD3_DEPOT=http://127.0.0.1:8991 python3 pipeline/validate_manifest.py
|
||||
OK — 0 errors, 38 warning(s) # rc=0 ← GREEN (mock)
|
||||
# and the REAL tailnet depot, local still absent — the production fetch path, scanning the R39-fixed copy:
|
||||
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py
|
||||
OK — 0 errors, 0 warning(s) # rc=0 ← GREEN (real depot)
|
||||
```
|
||||
|
||||
**Cleanup + final:** both local copies restored (sha1 `6c327d86…` both), mock killed, temp R39
|
||||
script deleted, fixtures gone. Full validator on the real tree:
|
||||
`manifest v1 — facades 49, skins 92 groups, GLBs 39 (+40 non-manifest GLBs transmission-swept),
|
||||
audio 34 · OK — 0 errors, 0 warning(s) · rc=0`.
|
||||
|
||||
### §40.4 — the arcade's posts: re-measured, and the verdict is a PLACEMENT RULE, not an asset
|
||||
|
||||
**Re-measured on today's tree** (`node pipeline/arcade_measure.mjs` + `buildings.js` read at the
|
||||
cited lines — all R39 numbers stand): one edge `kind:'arcade'` **42.00 m × 5 m**, lot faces ±2.50 m
|
||||
⇒ **5.00 m lane**; `AWNING_DEPTH 2.2` (`buildings.js:21`) at `AWNING_Y 2.95` (`:22`) ⇒ slabs reach
|
||||
±0.30 m ⇒ **0.60 m open slot** at the centreline; posts (`:641-646`) at `d/2 + 2.2 − 0.2` = 2.00 m
|
||||
from each lot face = **±0.50 m from the centreline — two rows 1.00 m apart**, 0.12 × 0.12 × 2.95 m,
|
||||
**34 posts** at seed 20261990 (2 × 17 shop lots), colliderless (`:649`).
|
||||
|
||||
**The kit (posts + roof designed together): the roof SPANS (depth 2.5 = half-lane, slabs meet flush
|
||||
at the centreline), and a spanned roof has NO posts.** That is a change in `web/js/world/buildings.js`
|
||||
+ one line of district plumbing in `web/js/world/chunks.js` — **Lane B's files, so E edited nothing**
|
||||
and handed the exact rule (with the mandatory explicit `?classic=1` gate — the synthetic IS the
|
||||
classic boot) to B in `LANE_E_NOTES.md` §40 with file:line.
|
||||
|
||||
**Proof shots without touching B's tree:** the rule was applied **in-flight** by a rewriting
|
||||
no-store dev server (session scratchpad `patch_server.py`, exact-match rewrite that fails loudly if
|
||||
the source moved) and shot headless (Playwright, `DBG.teleport(21.5, 66.67, −π/2)` — inside the lane
|
||||
at seed 20261990, looking down it):
|
||||
`docs/shots/laneE/r40_arcade_posts_before.png` (post rows + open slot) ·
|
||||
`r40_arcade_posts_after.png` (clear lane, continuous roof) · `r40_arcade_posts_pair.png`.
|
||||
**HUD draws 124 → 124 (+0), tris 44k → 44k, chunks 25 → 25.** Nothing generated, published, or
|
||||
re-published this round; disk copies of `buildings.js`/`chunks.js` byte-identical throughout.
|
||||
|
||||
## Round 12 (v3.0-alpha gig layer, `?gigs=1`) · 2026-07-15
|
||||
|
||||
|
||||
@ -1,5 +1,81 @@
|
||||
# LANE E — cross-lane notes
|
||||
|
||||
## Round 40 — the transmission gate grows eyes on every load path, and the arcade kit is a rule, not an asset ⟨40.1 + 40.4⟩
|
||||
|
||||
Shots: `docs/shots/laneE/r40_arcade_posts_before.png` · `r40_arcade_posts_after.png` ·
|
||||
`r40_arcade_posts_pair.png`. Full control-arm transcripts: `E-progress.md` § Round 40.
|
||||
|
||||
### → LANE F: §40.1 — the R39 blind spot is CLOSED, both arms demonstrated
|
||||
|
||||
R39 filed it live: `check_transmission()` parsed only `pipeline/_normalized/<file>` and returned
|
||||
silently when that copy was absent — a depot-only transmissive GLB, or a stale glass copy in
|
||||
`web/assets/models/` (**what `?localdepot=1` actually serves**), shipped unseen. `validate_manifest.py`
|
||||
now scans **every path the game can load a GLB from**:
|
||||
|
||||
| path | when scanned | how |
|
||||
|---|---|---|
|
||||
| `pipeline/_normalized/<file>` | always (as before) | local parse |
|
||||
| `web/assets/models/<file>` | **always — NEW** | local parse (staging and served copies CAN differ; that is the point) |
|
||||
| `<depot>/a/<file>` | **when NO local copy exists in either dir — NEW** | ranged GET of the GLB JSON chunk (206-aware; on a 200 it streams and slices, which is what the tailnet depot returns) |
|
||||
| any other `.glb` in either dir | **always — NEW** (ped rigs, dance clips, strays) | local parse — `rigs.js` loads these by literal path from the same served dir |
|
||||
|
||||
A depot fetch failure is a loud `WARN … depot copy UNSCANNED`, never a silent skip. Real tree:
|
||||
**rc 0, 0 errors, 0 warnings — 39 manifest GLBs + 40 non-manifest GLBs swept.** Both control arms
|
||||
were demonstrated with fixtures reachable ONLY via a previously-unscanned path (RED rc 1 on each,
|
||||
GREEN rc 0 after removal; the R39 script passes rc 0 over the same RED tree — the A/B is in
|
||||
E-progress). Depot arm used a **local mock depot** — nothing was published to the real namespace.
|
||||
`qa.sh` consumes rc only; the summary line grew `(+N non-manifest GLBs transmission-swept)`.
|
||||
|
||||
### → LANE B: §40.4 — THE ARCADE'S POSTS. Verdict: **PLACEMENT-RULE change in your files — E touched nothing.** The rule + numbers below; the roof and the posts are designed as ONE kit.
|
||||
|
||||
**Re-measured on today's tree** (`node pipeline/arcade_measure.mjs`, plus `buildings.js` read at the
|
||||
lines cited — all R39 numbers stand, nothing moved):
|
||||
|
||||
- ONE edge `kind:'arcade'`, **42.00 m × 5 m** (seed 20261990: (19.34,66.67)→(61.34,66.67)); lot
|
||||
faces at ±2.50 m ⇒ **5.00 m** between shopfronts; 16–17 shops over 5 seeds.
|
||||
- `buildings.js:21` `AWNING_DEPTH 2.2` · `:22` `AWNING_Y 2.95` · slab at `:637-639` reaches
|
||||
±0.30 m ⇒ a **0.60 m open slot** down the centreline.
|
||||
- `:641-646` plants two posts per shop lot at `d/2 + AWNING_DEPTH − 0.2` = **2.00 m out from the
|
||||
lot face = ±0.50 m from the centreline** — **two rows of posts 1.00 m apart down a 5.00 m lane**,
|
||||
0.12 × 0.12 × 2.95 m, **34 of them** at seed 20261990 (2 × 17 shop lots). No collider
|
||||
(`:649` `lotCollider` only), so walkability is untouched; it *reads* as a colonnade planted in
|
||||
its own doorway.
|
||||
|
||||
**THE KIT (posts + roof designed together): a real arcade's roof SPANS, and a spanned roof has no
|
||||
posts.** No new geometry — the roof is the existing awning-slab instances, deepened to meet; the
|
||||
posts are deleted instances. Exact rule, in your files:
|
||||
|
||||
1. **`web/js/world/chunks.js:13`** (`createChunkManager(plan, scene, ctx)`) — the builder needs
|
||||
district knowledge (`ctx` today is `{skins, citySeed, night}`; lots carry `block`, but
|
||||
block→district lives only in the plan):
|
||||
```js
|
||||
const kindById = new Map((plan.districts || []).map((d) => [d.id, d.kind]));
|
||||
ctx.arcadeBlocks = new Set((plan.blocks || [])
|
||||
.filter((b) => kindById.get(b.district) === 'arcade').map((b) => b.id));
|
||||
```
|
||||
Keyed on **`district.kind`, never `edge.kind`** — the R39 rule: real towns have 1,102
|
||||
arcade-KIND edges = 39 km of ordinary footpath and zero arcade DISTRICTS.
|
||||
2. **`web/js/world/buildings.js:635-646`** (the awning block in `buildShopfront`):
|
||||
`const inArcade = ARCADEKIT && ctx.arcadeBlocks?.has(lot.block);` then
|
||||
depth `inArcade ? 2.5 : AWNING_DEPTH` at `:637` and `:638` (2.50 = the half-lane: the slab's
|
||||
outer edge lands exactly on the centreline, opposing slabs meet flush, the 0.60 m slot closes),
|
||||
and wrap the post loop `:641-646` in `if (!inArcade)`.
|
||||
3. **The classic gate is MANDATORY and it is the explicit-flag pattern, not by-absence** (R39's
|
||||
warning, restated): the arcade district exists ONLY on the synthetic and **the synthetic IS the
|
||||
classic boot** — ungated, this moves the golden render. `ground.js:43-50`'s `VERGE` idiom:
|
||||
`ARCADEKIT = !classic && p.get('arcadekit') !== '0'` (`?classic=1` keeps today's geometry
|
||||
byte-identical, `?arcadekit=0` is F's falsifiability control).
|
||||
|
||||
**Cost, measured live** (in-flight patched serve of exactly the rule above — no Lane B file on disk
|
||||
was modified; `docs/shots/laneE/r40_arcade_posts_pair.png`): **draws 124 → 124 (+0)**, −34 `boxMats`
|
||||
instances, slab depth 2.2 → 2.5 on arcade lots only, everywhere else untouched. Nothing was
|
||||
generated, published, or re-published this round — no depot writes, no sha1 deltas.
|
||||
|
||||
**One kit note for later:** with the roof spanned, the slab's underside IS the lane's ceiling —
|
||||
that is exactly kit item #3's `ground-arcade-roof` skin (`use:"arcade-ceiling"`, shipped R39,
|
||||
selected by nothing *on purpose*). Wiring it stays your call at **+1 draw in the 2 chunks** that
|
||||
hold arcade lots (measured: lots span chunks 0,0 and 0,1 on all five seeds).
|
||||
|
||||
## Round 39 — the magpie settled, the arcade measured, and three GLBs that were secretly glass ⟨v9 39.4⟩
|
||||
|
||||
Full provenance and method: `pipeline/AUDIT.md` § Round 39. Sheets:
|
||||
|
||||
BIN
docs/shots/laneE/r40_arcade_posts_after.png
Normal file
BIN
docs/shots/laneE/r40_arcade_posts_after.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 551 KiB |
BIN
docs/shots/laneE/r40_arcade_posts_before.png
Normal file
BIN
docs/shots/laneE/r40_arcade_posts_before.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 560 KiB |
BIN
docs/shots/laneE/r40_arcade_posts_pair.png
Normal file
BIN
docs/shots/laneE/r40_arcade_posts_pair.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 644 KiB |
@ -5,15 +5,22 @@ Checks: JSON parses; every referenced skin exists locally; every fitting/furnitu
|
||||
exists locally (pipeline/_normalized) OR HEADs 200 on the depot; every thumb exists;
|
||||
footprints & heights are sane; every registry shop type has >=2 facades.
|
||||
|
||||
[R40 §40.1] The transmission gate scans EVERY path the game can load a GLB from:
|
||||
pipeline/_normalized/ (staging), web/assets/models/ (the ?localdepot=1 served mirror), and —
|
||||
for any manifest GLB with NO local copy in either — the depot copy itself (ranged GET of the
|
||||
GLB JSON chunk). Non-manifest GLBs in either local dir (ped rigs, dance clips, strays) are
|
||||
swept too: the game loads rigs by literal path from the same dirs.
|
||||
|
||||
python3 pipeline/validate_manifest.py # local + soft depot check
|
||||
python3 pipeline/validate_manifest.py --depot # require GLBs live on the depot (post-publish)
|
||||
|
||||
Exit 0 = green, 1 = fail. Plain stdlib (no deps), so it runs anywhere.
|
||||
"""
|
||||
import json, os, sys, urllib.request, urllib.error
|
||||
import json, os, struct, sys, urllib.request, urllib.error
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
ASSETS = os.path.join(ROOT, "web", "assets")
|
||||
MODELS = os.path.join(ASSETS, "models") # loaders.js LOCAL_MODELS — what ?localdepot=1 serves
|
||||
NORM = os.path.join(ROOT, "pipeline", "_normalized")
|
||||
MANIFEST = os.path.join(ASSETS, "manifest.json")
|
||||
REGISTRY_TYPES = ["record", "opshop", "toy", "book", "video", "pawn", "milkbar", "dept", "stall"]
|
||||
@ -49,47 +56,109 @@ def check_skin(file):
|
||||
err(f"skin missing: {file}")
|
||||
|
||||
|
||||
def check_transmission(local_path, file):
|
||||
"""[R39] HARD FAIL on KHR_materials_transmission > 0.
|
||||
# ── the transmission gate (R39, blind spot closed R40 §40.1) ─────────────────────────────────────
|
||||
# [R39] HARD FAIL on KHR_materials_transmission > 0.
|
||||
# This is a draw-budget check wearing a material's clothes. three.js runs a transmission PRE-PASS
|
||||
# — `renderTransmissionPass(opaqueObjects, …)`, three.module.js:16433, entered at :16209 whenever
|
||||
# `currentRenderList.transmissive.length > 0` — which re-renders the entire OPAQUE list into a
|
||||
# render target, so EVERY draw in that scene is issued twice. Lane C measured the effect of one
|
||||
# such material (the bookshelf's): opshop/hall 191→104 draws, book/hall 101→51, against a control
|
||||
# room with no bookshelf at 116→116. Three GLBs shipped with `transmissionFactor: 1` because a
|
||||
# bookshelf, a park bench and a streetlight are obviously not glass and so nobody ever looked.
|
||||
# A gate is the only thing that makes "nobody looked" impossible. Fix: pipeline/strip_transmission.py
|
||||
#
|
||||
# [R40 §40.1] The R39 gate only parsed pipeline/_normalized/<file> and returned silently when that
|
||||
# copy was absent — so a depot-only transmissive GLB (exactly the shape of longbench/streetlight)
|
||||
# or a stale glass copy in web/assets/models/ (what ?localdepot=1 actually serves) shipped unseen.
|
||||
# Demonstrated live in R39: the glass asset passed rc 0 with the local copy removed. Now every
|
||||
# load path is scanned: both local dirs always, the depot copy whenever no local file exists.
|
||||
|
||||
This is a draw-budget check wearing a material's clothes. three.js runs a transmission PRE-PASS
|
||||
— `renderTransmissionPass(opaqueObjects, …)`, three.module.js:16433, entered at :16209 whenever
|
||||
`currentRenderList.transmissive.length > 0` — which re-renders the entire OPAQUE list into a
|
||||
render target, so EVERY draw in that scene is issued twice. Lane C measured the effect of one
|
||||
such material (the bookshelf's): opshop/hall 191→104 draws, book/hall 101→51, against a control
|
||||
room with no bookshelf at 116→116. Three GLBs shipped with `transmissionFactor: 1` because a
|
||||
bookshelf, a park bench and a streetlight are obviously not glass and so nobody ever looked.
|
||||
A gate is the only thing that makes "nobody looked" impossible. Fix: pipeline/strip_transmission.py
|
||||
"""
|
||||
def _glb_json_chunk(d, label):
|
||||
"""Parse a GLB byte string → its JSON chunk dict, or None (warn on non-GLB magic is the
|
||||
caller's call — a skin or a rig is still a GLB here, so magic mismatch is worth a warning)."""
|
||||
if len(d) < 20 or d[:4] != b"glTF":
|
||||
warn(f"{label}: not a GLB (bad magic) — transmission unchecked")
|
||||
return None
|
||||
off = 12
|
||||
while off + 8 <= len(d):
|
||||
clen, ctype = struct.unpack("<II", d[off:off + 8])
|
||||
if ctype == 0x4E4F534A:
|
||||
return json.loads(d[off + 8: off + 8 + clen].decode("utf-8"))
|
||||
off += 8 + clen
|
||||
warn(f"{label}: no JSON chunk found — transmission unchecked")
|
||||
return None
|
||||
|
||||
|
||||
def _scan_transmission(g, label):
|
||||
"""err() every material with transmissionFactor > 0. g = glTF JSON dict (may be None)."""
|
||||
for i, mat in enumerate((g or {}).get("materials", [])):
|
||||
t = (mat.get("extensions") or {}).get("KHR_materials_transmission")
|
||||
if t and t.get("transmissionFactor", 0) > 0:
|
||||
err(f"{label}: material[{i}] '{mat.get('name')}' has transmissionFactor "
|
||||
f"{t['transmissionFactor']} — three.js will run a transmission PRE-PASS and "
|
||||
f"issue every opaque draw in that scene TWICE. Run pipeline/strip_transmission.py")
|
||||
|
||||
|
||||
def check_transmission(local_path, label):
|
||||
"""Transmission scan of one LOCAL GLB. Absent file = nothing to parse (the caller decides
|
||||
whether an absent file means the depot copy must be fetched instead — see check_glb)."""
|
||||
if not os.path.isfile(local_path):
|
||||
return # depot-only asset: nothing to parse here
|
||||
return
|
||||
try:
|
||||
import struct
|
||||
d = open(local_path, "rb").read()
|
||||
if d[:4] != b"glTF":
|
||||
return
|
||||
off, g = 12, None
|
||||
while off < len(d):
|
||||
clen, ctype = struct.unpack("<II", d[off:off + 8])
|
||||
if ctype == 0x4E4F534A:
|
||||
g = json.loads(d[off + 8: off + 8 + clen].decode("utf-8"))
|
||||
break
|
||||
off += 8 + clen
|
||||
for i, mat in enumerate(g.get("materials", []) if g else []):
|
||||
t = (mat.get("extensions") or {}).get("KHR_materials_transmission")
|
||||
if t and t.get("transmissionFactor", 0) > 0:
|
||||
err(f"{file}: material[{i}] '{mat.get('name')}' has transmissionFactor "
|
||||
f"{t['transmissionFactor']} — three.js will run a transmission PRE-PASS and "
|
||||
f"issue every opaque draw in that scene TWICE. Run pipeline/strip_transmission.py")
|
||||
_scan_transmission(_glb_json_chunk(open(local_path, "rb").read(), label), label)
|
||||
except Exception as e: # a parse failure is not a licence to skip the check
|
||||
warn(f"{file}: could not read materials for the transmission check ({str(e)[:60]})")
|
||||
warn(f"{label}: could not read materials for the transmission check ({str(e)[:60]})")
|
||||
|
||||
|
||||
def _ranged_read(url, start, length):
|
||||
"""GET url bytes [start, start+length). Honours 206; on a 200 (the tailnet depot and plain
|
||||
http.server both ignore Range) streams and slices so we never pull more than needed."""
|
||||
req = urllib.request.Request(url, headers={"Range": f"bytes={start}-{start + length - 1}",
|
||||
"User-Agent": "procity-validator/1.0"})
|
||||
r = urllib.request.urlopen(req, timeout=20)
|
||||
try:
|
||||
if r.status == 206:
|
||||
return r.read()
|
||||
skip = start
|
||||
while skip > 0: # 200 = full body; skip up to our offset
|
||||
chunk = r.read(min(65536, skip))
|
||||
if not chunk:
|
||||
return b""
|
||||
skip -= len(chunk)
|
||||
return r.read(length)
|
||||
finally:
|
||||
r.close()
|
||||
|
||||
|
||||
def check_transmission_depot(url, label):
|
||||
"""Transmission scan of the DEPOT copy — the JSON chunk via two ranged GETs (header first for
|
||||
the chunk length, then exactly the chunk). A fetch failure is a warning, never a silent skip."""
|
||||
try:
|
||||
head = _ranged_read(url, 0, 20)
|
||||
if len(head) < 20 or head[:4] != b"glTF":
|
||||
warn(f"{label}: depot copy is not a GLB (bad magic) — transmission unchecked")
|
||||
return
|
||||
clen, ctype = struct.unpack("<II", head[12:20])
|
||||
if ctype != 0x4E4F534A:
|
||||
warn(f"{label}: depot copy's first chunk is not JSON — transmission unchecked")
|
||||
return
|
||||
g = json.loads(_ranged_read(url, 20, clen).decode("utf-8"))
|
||||
_scan_transmission(g, f"{label} (depot copy)")
|
||||
except Exception as e:
|
||||
warn(f"{label}: depot transmission check failed ({str(e)[:60]}) — depot copy UNSCANNED")
|
||||
|
||||
|
||||
def check_glb(entry, depot):
|
||||
file = entry["file"]
|
||||
local = os.path.join(NORM, file)
|
||||
served = os.path.join(MODELS, file)
|
||||
on_disk = os.path.isfile(local)
|
||||
# [R40 §40.1] every path the game can load this GLB from: staging copy, the ?localdepot=1
|
||||
# served copy (they CAN differ — that is the point), and the depot when neither exists locally.
|
||||
check_transmission(local, file)
|
||||
check_transmission(served, f"{file} (assets/models copy)")
|
||||
if not on_disk and not os.path.isfile(served):
|
||||
check_transmission_depot(f"{depot}/a/{file}", file)
|
||||
live = head_ok(f"{depot}/a/{file}")
|
||||
if STRICT_DEPOT and not live:
|
||||
err(f"GLB not live on depot: {file}")
|
||||
@ -153,6 +222,17 @@ def main():
|
||||
manifest_files.add(entry["file"])
|
||||
n_glb += 1
|
||||
|
||||
# [R40 §40.1] sweep every OTHER .glb sitting in a game-loadable dir — ped rigs, dance clips,
|
||||
# anything staged but not (yet) in the manifest. rigs.js loads these by literal path from the
|
||||
# same served dir, and R39 proved "obviously not glass" is exactly what nobody looks at.
|
||||
n_swept = 0
|
||||
for d, tag in ((NORM, "pipeline/_normalized"), (MODELS, "web/assets/models")):
|
||||
if os.path.isdir(d):
|
||||
for f in sorted(os.listdir(d)):
|
||||
if f.endswith(".glb") and f not in manifest_files:
|
||||
check_transmission(os.path.join(d, f), f"{tag}/{f}")
|
||||
n_swept += 1
|
||||
|
||||
# audio pack (round-11): if the manifest names an audio file it must ship locally (both the
|
||||
# ogg primary and the m4a fallback). Silent-happy is a runtime rule, not a licence to dangle refs.
|
||||
n_audio = 0
|
||||
@ -200,7 +280,7 @@ def main():
|
||||
|
||||
print(f"manifest v{m.get('version')} — facades {len(facade)}, "
|
||||
f"skins {sum(len(v) if isinstance(v, list) else (len(v) if isinstance(v, dict) else 0) for v in sk.values())} groups, "
|
||||
f"GLBs {n_glb}, audio {n_audio}")
|
||||
f"GLBs {n_glb} (+{n_swept} non-manifest GLBs transmission-swept), audio {n_audio}")
|
||||
for w in warnings:
|
||||
print(f" WARN {w}")
|
||||
if errors:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user