Files
security-game/MEMORY.md
T
zwitschi 1ce173e9d1
Full build / Import + headless tests (push) Successful in 45s
Full build / Full desktop build + optional release (push) Skipped
Record the Quaternius pilot bake, measurements and face-gate decision point in the docs
2026-11-06 13:45:05 +01:00

290 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MEMORY
Session memory for durable decisions, gotchas, and commands. Implementation history lives in
`DONE.md`, user-facing changes in `CHANGELOG.md`, the live task list in `TODO.md`, and the
canonical implemented-features list in `docs/development-status.md`.
## Decisions
- **Local-only reference override (2026-11-03, owner):** development-only photo research ignores
the licence gate. Downloads live in `tmp/research/` (gitignored): DDG image search, street-view
frames, neighbouring buildings — never committed, never shipped. Promotion into
`docs/screens/refs/` always passes the sidecar licence + privacy gate and `CREDITS.md`.
- **AI-assisted asset review loop (2026-11-04):** head/face shape decisions use a concept +
judge loop — generate a target concept (`generate_textures.generate_image`, Seedream) and use
`tools/describe_image.py` as the vision judge, iterating until the judge's specific fixes
converge. Review pages: `docs/screens/head-review/README.md`, `docs/3d-model-review.md`
§AI-driven head rework.
- **Project agent skills (2026-11-03):** two tracked skills live in `.agents/skills/`:
`gig-line-blender-asset-contract` and `gig-line-release-qa`. They are guidance only,
add no runtime dependency and are referenced from `tools/README.md`. The third-party
`blender-skills` pack remains gitignored.
- **TV Studio shares the Hollywood location (owner):** the Late Night TV Studio is the same El
Capitan Entertainment Centre block at 6840 Hollywood Blvd (which houses *Jimmy Kimmel Live!*),
restaged for a late-night arrival. `VENUE_SPECS["tv_studio"]` is a 30.0 m variant of the 29.6 m
Hollywood facade. One reference board serves both venues; never create a separate TV Studio board.
- **Jimmy Kimmel Live = starting point:** the Hollywood first shift and the TV Studio arrival both
begin at the El Capitan block. Stated in `docs/venues.md#hollywood-boulevard` and `docs/gameplay.md`.
- **Street-view limitation:** Google Street View panoramas need a Google API key or manual browser
capture, so the local reference sets use DDG street-level photos instead.
- **Image-recitation filter:** Gemini image models reject well-known branded facades (Comedy Store
wall) as generation anchors (`IMAGE_RECITATION`). Workaround: anchor on a less-branded frame of
the same block, or generate text-only when the reference adds no geometry.
- **OpenRouter image-to-image:** `/v1/images` is text-to-image only; the working path is
`POST /v1/chat/completions` with a `content` array carrying a base64 `image_url`. Response
images arrive as `message.images[].image_url.url` (fallback: a data URI inside the message text).
Implemented in `generate_textures.generate_image(reference_path=...)` and
`scene_concepts.py generate/anchor`; provenance sidecars are `.refgen.json`.
- **Facade/venue material picks (2026-11-03):** the Masonic/El Capitan facade uses the
photo-anchored `limestone_ashlar` field (warm tint `Color(0.85, 0.81, 0.74)`) with `stone_steps`
for trim; the Joke Shop uses `dark_stucco`; the marquee canopy/sign box stay `black_metal` by
design (fictional "GIG LINE" marquee, not the red El Capitan Theatre marquee next door). The
TV Studio reuses `masonic_facade`. Committed Comedy Store refs were removed in favour of the
local-only `tmp/research/comedy-store/` set; the three-up board's photo panel is a historical
public-domain frame.
- **Godot 4.7.2 `--import` gotcha:** with a stale `.godot/` cache, new image files never import
(no `.import` created) and `preload()` of the new texture fails every dependent script with the
misleading `Could not resolve external class member "spec"`. Fix: import in a scratch copy with
`.godot/` removed, then copy back the `.png.import` and `.godot/imported/*.ctex/.md5`.
- **Blender skills pack (local aid):** MIT `blender-skills` at `.agents/skills/blender-skills/`
(94 skills + shared `references/`), gitignored, kept out of Godot's scanner by
`.agents/.gdignore`. Keep the pack flat when updating (`cp -a <pack>/.claude/skills/. ...`).
Pi gates project `.agents/skills` behind project trust.
- **Gaussian splatting (unversioned):** offline/development-only until an ADR proves licence,
privacy, Compatibility/Web cost and mesh collision. Accepted photo channels: team captures,
permissioned user/volunteer photos, clearly licensed downloads; every set needs a
provenance/consent sidecar. No runtime splat dependency in 0.10.0.
- **0.10.0 balance protocol:** `docs/release-qa.md#balance-acceptance` defines targets/denominators for the
36 venue/difficulty/phase cells, seeds 1–100, and access/emergency error rates. Gotcha:
`_read_npc_truth()` is type-derived, not behaviour-derived; the emergency-fairness review must
account for that oracle mismatch.
- **Blender MCP is optional (2026-11-04):** setup and safe part workflow are in
`docs/asset-tooling.md#optional-interactive-blender-mcp`. The headless flatpak scripts remain authoritative; MCP is
never a build/CI/test/runtime dependency, and anything MCP touched must pass the
CLI audit/fit gates.
- **Character mesh cap (2026-11-05):** all characters use 24,000 tris / 24,000 glTF vertices
(`CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in `tools/blender_assets.py`; `npc_pipeline.MAX_TRIANGLES`/
`MAX_VERTICES`). The required 13 core bones stay, optional extra bones are allowed, and the
low-poly `npc_base` fallback remains the shipped fallback. The raise funds the Quaternius base bake and
the 12-variant clothing detail. Measure the 24-NPC/Web budget after
any geometry change; never silently lower the cap without a new decision.
- **Quaternius is the NPC base (2026-11-05, owner):** ADR-0017 supersedes the generator-first
sourcing decision. The CC0 Quaternius Ultimate Modular character pack is the primary NPC mesh
and animation source. Stage 1 bakes a proportion-warped, 13-core-contract `npc_base` so
`NpcAvatar`, gear and animations keep working; the 62-joint/24-clip source rig is authoring
input, not a runtime dependency. If the warp fails the silhouette gate, Stage 2 needs a new ADR
for a 62-joint runtime, not a hybrid. The MakeHuman head and procedural `npc_base` remain
tested fallbacks until the Quaternius rollout ships. Quaternius source: CC0, attribution in
`CREDITS.md`; the three researched GLBs and licence notes are kept in
`tmp/npc-research/quaternius/`. MPFB/MakeHuman body work is closed; do not add new work there.
- **Quaternius bake keeps source detail (2026-11-05, owner):** during the contract bake (§2),
keep the Quaternius mesh as close to source as possible — no unnecessary decimation or LOD
reduction. The canonical base is only 3,199 verts / 6,206 tris, far under the 24,000 cap, so
there is no budget reason to decimate before the silhouette gate. Do not reuse the old
7,000-triangle or MPFB decimation targets.
- **Quaternius runtime rig (2026-11-05, owner, ADR-0018 option 2):** the Stage 1 proportion warp
failed the silhouette gate (arm ×1.63 / leg ×0.74 / torso ×1.61; vision judge: paddle hands,
lost forearms, stretched shins). The owner chose to **adopt the Quaternius 62-joint rig at
runtime** instead of warping: `NpcAvatar` maps the Quaternius joint names, gear attach points
are re-derived from the Quaternius skeleton (hip ≈0.87, shoulder ≈0.148/1.39, wrist ≈0.99,
knee ≈0.52), the mesh keeps source LOD (arm-down rest pose baked), and Idle/Walk play directly
on the 62-joint rig (no retarget).
- **Engine baseline:** Godot 4.7.2 stable (official editor `~/.local/bin/godot`). The system's
`godot-runner` cannot load arbitrary project paths (compiled without path overrides); use the
official editor binary.
- **GDScript/architecture:** cross-file references use explicit `preload()` constants (not
`class_name`) so scripts compile in headless `--script` runs; the test runner is a custom minimal
harness (not GUT, which needs the editor plugin); no autoload signal bus — `ShiftCoordinator`
owns the `EventBus` instance passed to subsystems.
- **Skinned NPCs (v0.6.0):** `NpcAvatar._build_skinned_body()` loads `npc_base.glb`; procedural
`_build_body()` is the fallback. Bone pose rotation is the **absolute local orientation**, not an
offset — compose with `get_bone_rest(i).basis` or the body snaps to a broken pose. Five tint
slots via `SurfaceMaterials.npc_slot()`; keep `_rig.scale` uniform on the skinned path.
- **NPC facing:** the avatar's visual front is **+Z**; Godot `look_at()` aims **-Z** (made NPCs
walk backwards). Use `NpcAvatar.yaw_towards(dir)` and set `rotation.y`; never `look_at()` on
avatars. Head-turn offsets use `+basis.z` for the same reason.
- **Release blockers (still open):** macOS unsigned, AI image provider terms/commercial rights not
cleared, storefront/rating approval pending, external playtests and full hardware matrix pending.
Recorded in `CREDITS.md`, `PRIVACY.md`, `docs/accessibility.md`, `docs/known-limitations.md`.
- **Release roadmap (2026-11-05, owner):** `0.9.9` Quaternius pilot base (rig/export fix,
rest-relative pose math, owner face/silhouette gate), `0.9.10` 12-role roster, `0.9.11`
scheduled venue/prop/lighting polish, `0.9.12` performance/Web/accessibility verification,
`0.10.0` public-readiness minor, then `0.10.1`–`0.10.3` fixes, `0.11.0` replay + fourth venue +
localization foundation, `0.12.0` signed/storefront builds + save v2, `0.13.x` content complete
and release candidates, `1.0.0` stable. The full gap analysis and roadmap live in `PLAN.md`; the
character track is versioned now, so a blocked rig cannot block the release. Planning only —
none of these versions is built yet.
- **Quaternius face/readability gate (2026-11-05):** the base is 6.2k tris total and replaces a
library with a sculpted realistic face, so the 0.9.9 gate is a product decision, not just an
art check. Owner picks full swap / crowd-only / extra head detail. Do not lower character
fidelity silently.
- **Pose composition is rest-relative (resolved 2026-11-05):** Godot's `set_bone_pose_rotation`
offsets from the bone rest, so the correct swing around a world axis is
`rest * Quaternion(rest.inverse() * axis, angle)`. For the old 180° rest this reduces to the
legacy `rest * Quaternion(axis, angle)`, so the 13-core fallback is unchanged (commit `8b1bf73`).
- **One character source, one fallback:** no new NPC source without an ADR. MPFB body work stays
closed, the Stage-1 proportion warp is not reopened, and the procedural `npc_base` + MakeHuman
head stay tested until the Quaternius base passes its gate.
## Gotchas / Known Issues
- **Two character builders — do not confuse them.** `tools/blender_assets.py` is the real builder
for the **eight library characters, 18 gear pieces and props** (it has the 0.9.4 face/hand
sculpting in `_face()`/`_hands_and_shoes()`); `tools/npc_pipeline.py` builds **only**
`assets/models/generated/npc_base.glb`, the frozen plain-primitive fallback. Reading
`npc_pipeline.py` to judge the library's face/hand detail is wrong.
- **Pose capture fixed (2026-11-04):** the harness was missing `set_physics_process(false)`/
`set_process(false)` on the avatar, so the FSM (`_advance_fsm` + `_move_toward_target`) ran every
tick, reset `arrived`/velocity and made `theater_pose()` revert to idle/rest. Disabling both fixes
it (sit vs idle diff 5.63, walk vs idle 2.29). The earlier "moving the `Rig` child does not
render" probe was a red herring — it set the position inside `_shoot()` in the same frame as
`root.get_texture().get_image()`, so it never rendered a frame. Lesson: pose/portrait harnesses
must freeze the avatar's process/physics before driving `_animate()`.
- Capture/pose harness gotchas: scripts that read captured-mouse input must run windowed
(`smoke_props.gd`); the NPC FSM rewrites `rotation.y` every physics tick, so pose/portrait
captures set the yaw after `set_physics_process(false)`; `visible=false` does not keep the crowd
out of a portrait frame (park the rest of the crowd away instead).
- Blender: `bisect_plane` and thin bevels leave zero-area triangles (Godot "non-finite normal in
LOD generation"); `_clean()` + a weld after bisect fixes them and the audit rejects them.
- Quaternius arm-down bake (working recipe, `tmp/npc-research/bake_armdown_v5.py`): pose-rotate
`UpperArm.L/R` −90° local X → read the pose matrices → apply the Armature modifier → **reset the
pose to identity** → set each arm bone's rest matrix from its pose matrix → re-add the modifier →
export `export_apply=False`. Missing the pose reset double-applies the arm-down (mesh ±0.53
instead of ±0.23). `armature_apply` in Blender 5.2 is broken (deforms the mesh but does not bake
the rest). The canonical base must be **join-only** (`build_base_minimal.py`: no `remove_doubles`
weld, no scale/ground transform, keep the Armature modifiers) — the weld and the scale/ground
transform corrupt the skin weights (arm poses to ±0.51 instead of −0.23).
- Quaternius `NpcAvatar` wiring (commits `2a6f29f`, `cd285b3`): `RIG_BONES` +
`NpcAvatar.bone_name(joint, side)` hold the **only** side mapping — the Quaternius labels `L`/`R`
opposite the project, so proxy side 0 (`-x`) drives `UpperLeg.R`/`UpperArm.R` while side 1 drives
the `.L` bones. The arm/elbow swing axis is `Vector3.LEFT` (legs stay `RIGHT`). The hand proxy is
`global_wrist − global_elbow` (subtracting the parent-relative `elbow.position` misplaces
handheld gear). Old 13-core names resolve through the same table.
- **Bone posing must write the global pose** (`NpcAvatar._swing_bone`): `reset_bone_pose()` then
`set_bone_global_pose(Transform3D(Basis(swing * global_rot), current_origin))`, conjugated with
`get_bone_global_rest()`. Both rejected alternatives fail: the rest-relative
`set_bone_pose_rotation(rest * Quaternion(rest⁻¹·axis, angle))` swings about the parent-relative
rest (the glTF importer already folded the axis conversion into it) and splayed the Quaternius
arms sideways; the plain `rest⁻¹ * swing * rest` conjugation flips the limbs 180°. Verified by
measuring `get_bone_global_pose()` after the call — posed bone axis matches the proxy pivot at
dot 1.000 on the Quaternius and the procedural rigs render unchanged.
Two caveats: children need the parent's swing passed in (the write is absolute), and
`set_bone_global_pose()` is a **no-op outside the scene tree**, so the headless runner (tests run
in `_initialize()`) cannot assert on it.
- **The Quaternius source UV map is degenerate** — every loop at (0,0), flat-colour materials, no
atlas. The AO/texture bake silently produces a 100 % black map, which tints every AO'd slot
(skin, hair, accessory) black in Godot: a black leg is the symptom. `build_base_minimal.py` now
Smart-UV-unwraps and repacks the layout (texel density 0 → 73–74 px/m on 256²), and the base
pipeline is two explicit steps with distinct files: `build_base_minimal.py`
(`quaternius/Casual_Hoodie.glb` → `quaternius_tpose.glb`, joins without welding, renames the
materials to the five tint slots, unwraps) then `bake_armdown_v5.py` (`quaternius_tpose.glb` →
`quaternius_base.glb` + `assets/models/blender_src/npc_casual.blend`). Re-running v5 on its own
output double-bakes the rest.
- Hand gear must be self-contained: the camera carried a 0.42 m neck-strap loop that floats when the
item is parented to the wrist. Hand items get a short wrist strap, not a neck strap. The backpack
straps were authored for the wider procedural chest and floated ~5 cm proud of the Quaternius
torso; the chest points now sit at x 0.12 / Godot z 0.15, which fits both.
- Image models have per-model size floors/caps (Seedream needs 1920+; `GEN_SIZE=2048`, do not
lower; gpt-5-image-mini caps lower). `generate_image` applies model-specific sizing; concept
generation defaults to `--request-size 1536`.
- Local photo research tooling: DDG rate-limits by IP (HTTP 202) — the `refcollect.py` token bucket
(0.5 req/s, burst 3) plus retry/backoff handles it; some DDG frames are AVIF mislabeled
`.png`/`.jpg` (convert with ffmpeg; `refcollect.py sheet` skips unreadable). Commons API needs a
descriptive User-Agent and rate-limits thumbnail downloads (fetch all metadata in one
`titles=A|B|C` query).
- Frozen decisions: do not re-open `override_access`; do not bind wand to `W`; do not remove the
radio easter egg.
- GDScript: no static signals; lambdas capture locals **by value** (mutating a captured local does
not affect the outer variable — use member functions for accumulators).
- Reference cycles: `FSM` holds its owner via `WeakRef`, `ShiftCoordinator` is `RefCounted`; if a
new subsystem introduces a cycle, repeat the WeakRef pattern.
- Headless (dummy renderer) logs `Parameter m is null` from `mesh_get_surface_count` — benign, does
not occur with a real GL driver.
- macOS export requires `rendering/textures/vram_compression/import_etc2_astc=true` (universal
build demands ETC2 ASTC).
- Typed spawn signal: handlers must accept the 4th `event: Dictionary` arg or Godot will not call
them.
- GDScript inference trap: a `var` typed as a base class (`radial: CanvasLayer`) exposes script
members as `Variant`; type it as the script type (`ActionRadial`). `--check-only --script`
cannot see the `Save` autoload — use a real boot (`--quit-after 180`) to verify full-scene
compile.
- Do not snapshot right after changing a transform in the same frame — `capture_screenshots.gd`
needs a state/frame gap or the capture uses the old camera orientation.
- `EventScheduler` probabilities are per simulated second, not per call; never poll it per frame.
- Light falloff: `omni_attenuation = 0.0` is **constant**, not physical, falloff in Godot 4.7 —
all local lights use `1.0`.
- glTF `baseColorFactor` is LINEAR — writing sRGB makes props pastel; `srgb_to_linear()` is applied
in `build_glb`. A new/changed `.glb` needs `godot --headless --import` before `load()` sees it.
- **Rebuilding a character GLB also requires re-baking its AO** (`tools/bake_ao.py -- --only
<name>`). `blender_assets.py build` writes the GLB only; the old `<name>_ao.png` then maps onto
the new geometry and produces large dark cheek/jaw blotches. Bake the AO, re-import, and refresh
the stale `.godot` cache (scratch-copy import) before judging a face/body change.
- `Save.SETTINGS_FILE` is declared and unused; settings persist through
`load_settings()`/`save_settings()`.
- Crowd reactions emit `event_bus.crowd_changed`; `GameManager._connect_crowd()` must reconnect
after every `coordinator.setup()` (same class of bug as the radio-dispatch reconnect).
- Packaging: NSIS uses `-D` defines (never `/D`) and resolves relative defines against the script
directory (pass `$PWD`-absolute paths); installed exe must be space-free; ICO must be classic
BMP entries (PIL emits PNG-compressed ICOs NSIS can't load). appimagetool runs system
`appstreamcli` — pass `--pedantic`.
- Credentials: `~/.git-credentials` holds the Gitea creds; `.env` holds only `OPENROUTER_API_KEY`.
- `assets/textures/game_ready/` is not imported (no `.import`) — use the imported
`assets/textures/skyline.png` path.
- Godot 4.7 writes `*.gd.uid` files — kept out of `git status` via `.git/info/exclude`.
- Collaborators occasionally leave uncommitted work in the shared tree; check `git status` before
committing and leave foreign changes untouched.
## Commands
- Tests: `godot --headless --path . --script res://src/test/test_runner.gd`
- Boot check: `godot --headless --path . --quit-after 120`
- Single screenshot: `godot --path . -s res://src/test/take_screenshot.gd`
- Staged gallery: `tools/check_screenshots.sh` (re-run the whole script, never a partial capture)
- Blender build: `flatpak run org.blender.Blender --background --python tools/npc_pipeline.py -- build`
(flatpak cwd is `/app/blender` and cannot read `/tmp` — use absolute repo paths; `.gdignore`
markers keep Godot from importing `.blend`/`tmp`)
- Textures: `python3 tools/generate_textures.py` (then `godot --headless --import`); AI mode
`python3 tools/generate_textures.py --ai`; general pipeline `python3 tools/asset_pipeline.py`
- Character checks: `python3 tools/blender_assets.py inspect|audit|fit`,
`python3 tools/npc_proportions.py`
- Docs: `python3 tools/check_docs.py` (every tracked image must be linked from its folder README)
- Head/part pipeline: `python3 tools/npc_parts.py build|extract|assemble|inspect|sheet|compare`
(Blender subcommands run under flatpak). Head sources: `build_character(head="face"|"proto"|"mh")`.
- Reference research: `python3 tools/refcollect.py search <q> --slug <block> --limit N`
(local-only), `tools/refboard.py` (Commons), `tools/three_up.py compose/rows`,
`tools/scene_concepts.py generate/anchor/realism`
## Open Questions
- Whether the responder avatar should be playable/controllable or purely scripted.
- Quaternius scope: full 12-role swap, crowd-only, or extra head detail (owner decision at 0.9.9).
- Localization target languages beyond English (0.11.0/0.12.0).
- macOS notarization approach for broad distribution (0.12.0).
- Fourth-venue choice for 0.11.0 (candidate: TCL Chinese Theatre forecourt).
## Current State
v0.9.8 is published. The build covers the three venues, the shift loop, tutorial/campaign/
checkpoints, the 0.9.3–0.9.8 polish/texture/integration/character passes and the Blender +
realism asset pipeline (see `DONE.md` and `CHANGELOG.md`). The 12-character procedural library
(realistic face + four crowd variants) and the Comedy Store map rebuild are complete.
**Planning pivot (2026-11-05, owner):** the character and map work was re-sequenced into
patches (`0.9.9`–`0.9.12`) with `0.10.0` as the public-readiness minor. The gap analysis and the
three release roadmaps now live in `PLAN.md`; `TODO.md` is the executable task list. ADR-0017
Stage 1 (proportion warp) is dead; ADR-0018 option 2 (62-joint runtime) is accepted.
**Quaternius pilot (2026-11-05, commits `cd285b3`, `fb3e75a`, `90f189a`):** `npc_casual` ships as
the Quaternius base. It renders arm-down, carries the five tint slots, plays the Idle/Walk/Sit pose
code on the 62-joint rig, and the procedural rigs are visually unchanged. Verified: `inspect`
(no issues), `audit` OK (6,206 tris / 12,988 verts, 62 bones), `fit` head 0.201 m, texel density
73–74 px/m, `npc_proportions` measured, 24-NPC budget smoke OK (168 meshes, 162,304 tris,
0.82 ms avg frame). The face/silhouette gate sheet and the three options are in
`docs/screens/npc-face-gate/`; the **owner decision is the only thing left before the 0.9.9
rollout**.
Test counts as of 2026-11-05: Python 167/167, Godot 278/278, `check_docs` OK.
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->