feat: clothing lane, character sources, and DCC bridges
Bulk import of the working lanes that were living untracked on the PC. Content: - characters/ Lena/male body lanes, bakes, texture work, run logs - clothing/ garment pipeline, configs, gates, contract docs - garments/ MD-authored garment sources (.zprj/.zpac) - UAL-Lib/ Universal Animation Library 2 source (.blend/.fbx/.glb) - tools/ blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent - docs/, plans/, dev/, .agents/plans/ Repo hygiene: - .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out on purpose — ~250 are already tracked raw and converting them would rewrite every one without shrinking history. - .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies dropped by a `>/dev/null` redirect on Windows). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
# Pipeline contract — **AS-BUILT (2026-07-31)**
|
||||
|
||||
This started as the build agreement between five parallel agents. It is now the
|
||||
as-built description of what actually exists and what the interfaces really are;
|
||||
where the original agreement and the code diverged, **the code won and this file
|
||||
was corrected**. Source design:
|
||||
`.agents/plans/clothing-pipeline-unification-2026-07-31.md`.
|
||||
|
||||
Corrections against the original (2026-07-31 pre-build) text are flagged
|
||||
**[as-built]** so anyone holding the old version can see what moved.
|
||||
|
||||
## File ownership (parallel build — do not edit outside your lane)
|
||||
|
||||
| Owner | Files |
|
||||
|---|---|
|
||||
| A (orchestrator) | `clothing/garment.py` |
|
||||
| B (lint gates) | `clothing/gates/g2_census.py`, `clothing/gates/g8_catalog_lint.py` |
|
||||
| C (pose gates) | `clothing/gates/g5_posed_sweep.py` (G3 = its `--rest` mode) |
|
||||
| D (MD template) | `tools/tailor/draft_garment.py`, `tools/tailor/qc_placement.py`, `clothing/gates/g1_drape.py` |
|
||||
| E (game side) | `ariki-game/tools/targeted_reimport.sh`, `ariki-game/tools/clothing_motion_qa.sh`, `ariki-game/src/Testing/ClothingTestBed.cs` (env override only) |
|
||||
|
||||
**Nobody** touches: `clothing/garment_pipeline.py`, `configs/pari.json`, `configs/piupiu.json`
|
||||
(weight-fix agent owns those), `configs/piupiu_sb.json`, `.agents/wiki/**`. New test/demo
|
||||
configs go in `clothing/configs/tests/`. No commits, no pushes.
|
||||
|
||||
## Config resolution
|
||||
|
||||
- `garment.py` resolves `extends` (single parent, path relative to `configs/`): deep-merge
|
||||
child over parent; **arrays and scalars replace whole**, objects merge per-key.
|
||||
- The resolved config is written to `work/<name>/resolved.json`. Every stage and gate is
|
||||
invoked with that file, never the raw config. `<name>` comes from the child config.
|
||||
|
||||
### Path absolutization **[as-built]**
|
||||
|
||||
Before `resolved.json` is written, `garment.py` rewrites these fields from relative to
|
||||
**absolute**, resolving against the `clothing/` directory (the same base
|
||||
`garment_pipeline.py` uses as `HERE`). Already-absolute values are untouched:
|
||||
|
||||
```
|
||||
source · body · parts.*.texture · md.avatar_fbx · md.zfab · md.texture
|
||||
md.snapshot · md.presim_snapshot · md.export_dir · export.out_dir
|
||||
```
|
||||
|
||||
This is not cosmetic. The three consumer families disagree about what a relative path
|
||||
is relative to:
|
||||
|
||||
| Consumer | Base for a relative path |
|
||||
|---|---|
|
||||
| `garment_pipeline.py` | `HERE` = `clothing/` (`garment_pipeline.py:265`, `:1005`) |
|
||||
| `tools/tailor/draft_garment.py` | the **config file's own directory**, then repo root |
|
||||
| `clothing/gates/g1_drape.py` (`_resolve`) | the **config file's own directory**, then repo root, then cwd |
|
||||
|
||||
`resolved.json` lives in `work/<name>/`, so `"texture": "../tools/tailor/textures/piupiu.png"`
|
||||
resolves correctly for the pipeline and to the non-existent `clothing/work/tools/...` for
|
||||
the other two. Absolutizing at the resolver makes all three agree. Asserted by
|
||||
`garment.py --selftest`. **Config authors keep writing relative paths** — they are
|
||||
relative to `clothing/`, as they always were.
|
||||
|
||||
## Stage list (orchestrator)
|
||||
|
||||
```
|
||||
draft drape publish | census prepare fit reduce bake skin export | register import verify
|
||||
+---- MD bridge ----+ +-------- Blender headless -------------+ +---- ariki-game ----+
|
||||
```
|
||||
|
||||
- Blender stages shell to (unchanged CLI):
|
||||
`"$BLENDER" --background --python clothing/garment_pipeline.py -- --config work/<name>/resolved.json --stage <s>`
|
||||
where `BLENDER` defaults to `C:/Program Files/Blender Foundation/Blender 5.1/blender.exe`
|
||||
(env `BLENDER` or `--blender` overrides).
|
||||
- MD stages: `python tools/tailor/draft_garment.py --config work/<name>/resolved.json --emit work/<name>/md_script.py --stage <draft|drape|publish>`
|
||||
then `python tools/md_bridge.py --file work/<name>/md_script.py --timeout <n>`;
|
||||
orchestrator pings first (`md_bridge.py --ping`) and fails fast with the human
|
||||
instruction ("click Plugin > TinqsMDBridge in MD") when no session.
|
||||
**[as-built]** `draft_garment.py` does accept `--stage` (`draft|drape|publish|all`,
|
||||
default `all`) and additionally `--timeout-hint`, which prints the MD-bridge timeout
|
||||
this garment needs instead of emitting.
|
||||
- `register`: emit-not-edit — write `work/<name>/register.cs.txt` containing the exact
|
||||
`Add(...)` line(s) + `BaseDirFor` case if the set's folder is new. Data comes from
|
||||
`catalog` + `export` + `parts[].slot`. **[as-built]** the `BaseDirFor` res:// path is
|
||||
derived from where `export.out_dir` sits **inside the game repo**, not from its
|
||||
basename; an out_dir outside the repo emits a placeholder plus a loud note.
|
||||
- `import`: agent E's `targeted_reimport.sh` — see the CLI below.
|
||||
- `verify`: agent E's `clothing_motion_qa.sh` — see the CLI below.
|
||||
- `bake` is skipped unless the resolved config sets `"bake": true` (matches today's
|
||||
"optional, slow" reality).
|
||||
|
||||
### Agent E's real CLIs **[as-built]**
|
||||
|
||||
The original contract sketched these as `bash <script> <args...>`. The built interfaces:
|
||||
|
||||
```bash
|
||||
# import (gate G7)
|
||||
bash <game>/tools/targeted_reimport.sh --report <work>/qc/g7.json <asset paths...>
|
||||
# assets accept repo-relative, absolute, or res:// paths
|
||||
# also: --dry-run (show which .godot/imported entries would be deleted, then stop)
|
||||
# exit 0 reimported+verified · 2 verification failed · 3 could not evaluate
|
||||
|
||||
# verify (gate G6)
|
||||
BED_SET=<Set> BED_GENDER=<0|1> \
|
||||
bash <game>/tools/clothing_motion_qa.sh \
|
||||
--clips "Idle Walk Dance" --frames 3 \
|
||||
[--bless <baseline-dir> | --diff <baseline-dir>] \
|
||||
--report <work>/qc/g6.json \
|
||||
<out-dir>
|
||||
# exit 0 pass · 2 outfit load error or a diffed frame over --max-diff · 3 could not spawn
|
||||
```
|
||||
|
||||
- `BED_GENDER` is `0 = Male`, `1 = Female` (`ClothingTestBed.cs:27`). `garment.py` derives
|
||||
it from `export.gender` (leading "F" → 1).
|
||||
- Baselines live at `<animation>/clothing/baselines/<slug>/`; capture out-dir is
|
||||
`<game>/.game-cli/clothing-qa/<slug>`. `<slug>` is `export.set` lowercased (agent E's own
|
||||
example: `BED_SET=Kapahaka` → `.../kapahaka`). Override with the config's optional
|
||||
`verify` block: `slug`, `baseline_dir`, `out_dir`, `clips`, `frames`.
|
||||
- **Bless flow.** `garment.py` picks the mode from the filesystem, not from a flag nobody
|
||||
remembers:
|
||||
- baseline dir exists → `--diff` (regression tripwire).
|
||||
- `GARMENT_BLESS=1` in the environment, or `--bless-baseline` on the command line →
|
||||
`--bless` (freeze the captured frames as the baseline).
|
||||
- neither → run with **no** `--bless`/`--diff`: the hard signal (outfit load errors on
|
||||
`/console` + engine stdout) still applies, the frames are captured as evidence, and a
|
||||
prominent banner tells you to look at them and then re-run with `GARMENT_BLESS=1`.
|
||||
Blessing before looking freezes a defect as the thing every future run is compared to.
|
||||
- Both scripts speak the gate exit-code contract and write their own `qc/*.json`, so
|
||||
`garment.py` treats an exit 2 from `import`/`verify` as a **gate** failure (honouring
|
||||
`--no-gate-stop`), not a tool crash.
|
||||
|
||||
## Gate contract
|
||||
|
||||
- Plain-python gates: `python clothing/gates/<gate>.py --config <resolved.json> --work work/<name>`
|
||||
- Blender gates: `"$BLENDER" --background --python clothing/gates/<gate>.py -- --config <resolved.json> --work work/<name>` (+ gate-specific flags)
|
||||
- **[as-built] which is which:** `g5_posed_sweep.py` is the **only** Blender gate (it
|
||||
imports `bpy` and opens `20_fit.blend`/`50_skin.blend`). `g1`, `g2`, `g8` are plain
|
||||
python (stdlib, plus PIL for g1). Overridable per gate in the resolved config:
|
||||
`"gates": {"g5": {"kind": "plain"}}`.
|
||||
- Exit codes: **0 pass · 2 fail (thresholds violated) · 3 error (could not evaluate)**.
|
||||
- Every gate writes `work/<name>/qc/<gate_id>.json`:
|
||||
|
||||
```json
|
||||
{ "gate": "g5", "pass": false, "checked_at_stage": "skin",
|
||||
"metrics": { "...": 0 }, "failures": [ {"part": "...", "detail": "...", "frame": "Walk[Walk_Fwd_Loop]@13"} ],
|
||||
"artifacts": ["qc/g5_frame_012.png"] }
|
||||
```
|
||||
|
||||
**[as-built] `failures[].frame` is a STRING label**, not an int — `"rest"`,
|
||||
`"Walk[Walk_Fwd_Loop]@13"`, `"Synthetic:arm_raise_70"`. A bare frame number is ambiguous
|
||||
once more than one clip is sampled.
|
||||
|
||||
**[as-built]** every entry in G5's `metrics.per_frame` also carries
|
||||
`"source": "rest" | "clip" | "synthetic"`, and `metrics` splits the labels into `clips`
|
||||
and `synthetic` (+ `synthetic_frames` count) — a mixed sweep (`pose_source: "both"`,
|
||||
i.e. `--qc deep`) is then readable without parsing label strings.
|
||||
|
||||
### Orchestrator gate map **[as-built]**
|
||||
|
||||
| Stage | Gate | Invocation | Runs when | Report file |
|
||||
|---|---|---|---|---|
|
||||
| `drape` | G1 | plain | `expect.bands` present | `qc/g1.json` |
|
||||
| `census` | G2 | plain | `expect.islands` present | `qc/g2.json` |
|
||||
| `fit` | **G3** | Blender, `g5_posed_sweep.py --rest` | `expect.penetration` present | **`qc/g3.json`** |
|
||||
| `skin` | G5 | Blender, `g5_posed_sweep.py` | `expect.penetration` present | `qc/g5.json` |
|
||||
| `register` | G8 | plain | always | `qc/g8.json` |
|
||||
| `import` | G7 | E's script | always (stage IS the gate) | `qc/g7.json` |
|
||||
| `verify` | G6 | E's script | always (stage IS the gate) | `qc/g6.json` |
|
||||
|
||||
The `fit` row is the one that trips people: **there is no `g3_*.py`**. G3 is
|
||||
`g5_posed_sweep.py --rest`, it sets `"gate": "g3"` / `"checked_at_stage": "fit"` in its
|
||||
report, and it writes **`qc/g3.json`**, not `qc/g5.json`. Gate failure stops the run
|
||||
unless `--no-gate-stop`.
|
||||
|
||||
## QC modes **[as-built]**
|
||||
|
||||
`garment.py --qc light|deep`. A mode is a **profile**: a small dict of sampling/cost
|
||||
overrides merged into the resolved config **at resolve time**, before `resolved.json` is
|
||||
written. Gates stay mode-unaware — they receive the same file they always did, with
|
||||
different numbers in it. Owned by `garment.py` (`QC_PROFILES`, `apply_qc_mode()`).
|
||||
|
||||
| | `light` (default) | `deep` |
|
||||
|---|---|---|
|
||||
| `expect.penetration.clips` | `["Walk"]` | `["Idle", "Walk", "Dance"]` |
|
||||
| `expect.penetration.frames_per_clip` | 2 | 8 |
|
||||
| `expect.penetration.anim_pack` | `[UAL2.glb]` | `[UAL1.glb, UAL2.glb]` |
|
||||
| `expect.penetration.pose_source` | `auto` | `both` (clips **+** synthetic extremes) |
|
||||
| `expect.penetration.max_render_frames` | 2 | 12 |
|
||||
| `verify.clips` / `verify.frames` | `"Walk"` / 1 | `"Idle Walk Dance"` / 3 |
|
||||
| gate failure | **reports**, run continues | **stops** the run |
|
||||
|
||||
`light` uses UAL2 alone because `Walk` aliases onto **UAL2's** `Walk_Fwd_Loop`; the
|
||||
second pack costs ~7 s of import and contributes nothing when Idle/Dance are not
|
||||
sampled. Measured on `configs/tests/piupiu_sb_test.json` `--from census --to export`:
|
||||
**33.3 s light vs 42.6 s deep** end to end, all of the delta in G5 (9.9 s vs 19.1 s).
|
||||
|
||||
### Which keys a profile may touch
|
||||
|
||||
**Only these** (`QC_PROFILE_KEYS` in `garment.py`):
|
||||
|
||||
```
|
||||
expect.penetration : clips frames_per_clip anim_pack pose_source max_render_frames
|
||||
verify : clips frames
|
||||
(top level) : gate_stop
|
||||
```
|
||||
|
||||
Anything else in a config's `qc.<mode>` block is **dropped with a warning** —
|
||||
`sanitize_qc_profile()`. A profile can therefore never move a THRESHOLD: `depth_mm`,
|
||||
`max_verts`, `max_verts_rest`, `gape.*` budgets, `expect.islands`, `expect.bands`,
|
||||
`expect.budget` belong to the config author in both modes. Switching modes changes how
|
||||
hard the pipeline looks, never what counts as a defect.
|
||||
|
||||
`expect.penetration` is only touched when it **already exists** — its presence is what
|
||||
arms G3/G5 (see the gate map), so a profile must not conjure a gate the author never
|
||||
asked for. A `verify` block is created if missing (harmless: the stage runs only when
|
||||
named).
|
||||
|
||||
### Precedence
|
||||
|
||||
```
|
||||
builtin QC_PROFILES[mode] -> config `qc.<mode>` block -> CLI
|
||||
```
|
||||
|
||||
for the cost knobs above, applied **over** whatever the config's own `expect`/`verify`
|
||||
says for those keys — a mode that could not override the config as written would not be
|
||||
a mode. Everything outside the whitelist keeps the config's value. A config that wants
|
||||
to keep its own value for one cost knob lists it in `qc.pin`:
|
||||
|
||||
```jsonc
|
||||
"qc": {
|
||||
"default_mode": "deep", // used when --qc is omitted
|
||||
"pin": ["clips", "verify.frames"], // shorthand = expect.penetration.<key>
|
||||
"light": { "expect": { "penetration": { "frames_per_clip": 4 } },
|
||||
"gate_stop": true },
|
||||
"deep": { "expect": { "penetration": { "clips": ["Idle", "Walk", "Dance", "Haka"] } } }
|
||||
}
|
||||
```
|
||||
|
||||
Mode selection: `--qc` > `qc.default_mode` > `light`.
|
||||
|
||||
### Stopping, and the reminder
|
||||
|
||||
`light` implies `--no-gate-stop` (failures are reported, the run finishes, exit code is
|
||||
still 2). **`--gate-stop`** forces stop-on-failure inside light; `--no-gate-stop` forces
|
||||
continue inside deep; the two flags together are an error. CLI beats the profile either
|
||||
way. A run with `--qc` omitted prints
|
||||
`QC mode: light -- run --qc deep before sign-off` at the end of the summary.
|
||||
|
||||
### Recorded in `resolved.json`
|
||||
|
||||
`_resolved.qc_mode` carries the applied mode and the effective knob values, so a QC
|
||||
report can always be traced to how hard it looked:
|
||||
|
||||
```jsonc
|
||||
"_resolved": { "qc_mode": {
|
||||
"mode": "light", "source": "default", // or "--qc" / "config qc.default_mode"
|
||||
"gate_stop": false, "gate_stop_source": "profile",
|
||||
"applied": { "expect.penetration.clips": ["Walk"], "...": "..." },
|
||||
"pinned": [], "notes": [] } }
|
||||
```
|
||||
|
||||
The same values are printed in the run banner.
|
||||
|
||||
## `expect` block schema
|
||||
|
||||
Contract baseline (unchanged — a config carrying only these keys still works):
|
||||
|
||||
```jsonc
|
||||
"expect": {
|
||||
"islands": { "count": 7, "mapped": { "0": {"verts": [4000, 6000], "z": [0.90, 1.35]} } },
|
||||
"penetration": {
|
||||
"max_verts": 0, "depth_mm": 1.0, // garment verts inside body
|
||||
"gape": { "<part>": {"band_z": [0.95, 1.30], "max_exposed_verts": 0} },
|
||||
"clips": ["Idle", "Walk"], // g5 posed mode; pack clips by name
|
||||
"frames_per_clip": 6
|
||||
},
|
||||
"bands": { "<band-name>": {"top_m": 1.31, "bottom_m": 1.05, "tol_m": 0.03} },
|
||||
"budget": { "<part>": {"tris": 2200, "min_iou": 0.985} }
|
||||
}
|
||||
```
|
||||
|
||||
Each gate's docstring is the authority on its own extensions. As built:
|
||||
|
||||
### `expect.islands` (G2) **[as-built]**
|
||||
|
||||
Extra keys beyond the baseline: `require_all_mapped` (bool; unmapped non-trivial islands
|
||||
fail instead of warn) and, per mapped island, `tris`, `z_min`, `z_max`, `z_span`, `x`.
|
||||
`mapped` keys may be an island index **or** a part name (when that part maps exactly one
|
||||
island). Every range accepts `[lo, hi]` (either endpoint may be `null`), a bare number
|
||||
(exact match), or `{"min":, "max":}`.
|
||||
|
||||
**UNITS: census units, i.e. pre-scale source-mesh units, NOT metres.** `align.scale_z`
|
||||
is applied later in `prepare`, so the piupiu census reads `z 5.184..11.126`, not
|
||||
`0.52..1.11`. Read the numbers off `work/<name>/census.json` rather than converting.
|
||||
|
||||
G2 also always cross-checks that every index in `parts[*].islands` exists in
|
||||
`census.json`, with or without an `expect` block — that is the check that catches an MD
|
||||
re-export silently renumbering islands.
|
||||
|
||||
### `expect.penetration` (G3/G5) **[as-built]**
|
||||
|
||||
All optional, all defaulted:
|
||||
|
||||
| Key | Default | What |
|
||||
|---|---|---|
|
||||
| `anim_pack` | both `anim/UAL1.glb` and `anim/UAL2.glb` | GLB(s) to pull clips from. Both, because the game binds idle/dance from UAL1 but aliases "walk" onto UAL2's `Walk_Fwd_Loop`. |
|
||||
| `clip_aliases` | built-in map mirroring the game's bindings | `{"Idle": "Idle_Loop", ...}`; then fuzzy match (shortest containing name wins). |
|
||||
| `pose_source` | `"auto"` | `auto` \| `clips` \| `synthetic` \| `both`. `auto` = clips, falling back to synthetic extremes when nothing resolves. **[as-built]** `both` = the clips **and then** the synthetic extremes (what `--qc deep` sets); it degrades to synthetic-only if no clip resolves. **The gate never passes for lack of animation.** |
|
||||
| `ignore_parts` | `[]` | Parts to skip. `"type": "bodyshell"` parts are always skipped — they *are* the body. |
|
||||
| `max_render_frames` | 6 | Cap on failing-frame QA PNGs (budget is spread across clips). 0 disables. |
|
||||
| `max_verts_rest` | = `max_verts` | Separate, looser rest-pose budget in full mode. |
|
||||
|
||||
`gape.<Part>` entries: `band_z` **required** (`[lo, hi]`, rest-pose world Z); optional
|
||||
`band_x`, `band_y`, `facing` + `facing_min`, `ray_mm` (50), `gap_mm` (25; a hit farther
|
||||
than this counts as exposed — ballooned fabric), `max_exposed_verts` (0),
|
||||
`max_exposed_delta` (threshold on posed-minus-rest exposure; when both are present a
|
||||
frame must satisfy both), `check_rest` (false), `parts` (which garment parts count as
|
||||
cover). Band vertices are selected **once in rest pose** and the same indices re-tested
|
||||
every frame, because anatomy is stable and posed Z is not.
|
||||
|
||||
### `expect.bands` + `expect.drape` (G1) **[as-built]**
|
||||
|
||||
`expect.bands` is what schedules the gate. Per band: `top_m`, `bottom_m`, `tol_m`
|
||||
(+ per-edge `top_tol_m` / `bottom_tol_m`), `from` (`"cover"` default = rows where the
|
||||
garment covers ≥ `min_row_cover` of the body's width, i.e. the band proper; `"extent"` =
|
||||
the loose mask, which also picks up straps, ties and fringes), `band_index`.
|
||||
|
||||
`expect.drape` carries the measurement parameters: `snapshot`, `height_m` (1.777),
|
||||
`landmarks`, `min_row_cover` (0.40), `min_row_extent` (0.05), `scale`, `z_range`, and
|
||||
**`mask`**. G1 is image-based because MD exposes no mesh introspection to Python
|
||||
(`GetClothPositions()` returns nothing), so it measures the rendered viewport.
|
||||
|
||||
**The `mask` block is effectively required in practice.** Its default mode `"auto"` is a
|
||||
crude skin/background heuristic that counts baked-in underwear and shaded skin as
|
||||
garment. Any garment with a generated texture — i.e. all of ours — needs
|
||||
`"mask": {"mode": "colors", "colors": [[173, 37, 39]], "tol": 30}`
|
||||
(or `"mode": "sat"`). Snapshot resolution order: `--snapshot` > `expect.drape.snapshot` >
|
||||
`md.snapshot`.
|
||||
|
||||
## Shared facts
|
||||
|
||||
- Body GLBs: `ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb`
|
||||
(+ `_SkirtRig` variant). 65-bone Quaternius skeleton, Lena height 1.777 m.
|
||||
- Anim packs for posed sweeps: `ariki-game/assets/quaternius/anim/` (UAL GLBs) — verified
|
||||
to be the **same 65-bone skeleton** as the derived-body RIG (exact name-set match), so
|
||||
G5 assigns actions directly with no retarget. Rigs with EXTRA bones (piupiu_sb's 16
|
||||
skirt bones) are fine: unanimated bones ride their parents.
|
||||
- Blender 4.4+/5.x **slotted actions**: an action with no bound `action_slot` animates
|
||||
nothing, silently. G5 binds the slot explicitly; anything else posing a rig must too.
|
||||
- Existing checkpoints for testing: `clothing/work/piupiu/`, `work/piupiu_sb/`, `work/pari/`
|
||||
— `50_skin.blend` contains `BODY`, `RIG`, `GARM_*`, `BODY_SHELL`.
|
||||
- Known-bug ground truth for G5 validation: pari neckline gapes at sternum in Walk/Dance;
|
||||
a thigh punches through the piupiu skirt. A correct G5 FAILS on these checkpoints.
|
||||
**[as-built]** the piupiu thigh poke reproduces headlessly and G5 catches it. The pari
|
||||
neckline gape does **not** reproduce in the Blender checkpoint — it is not present in
|
||||
the skinned mesh G5 measures, so its origin is in-game (attach/material/LOD), which
|
||||
makes it G6 territory rather than G5.
|
||||
- Game agent API (when a bed is spawned): `/health /screenshot /navigate /state /scene /ui /console`.
|
||||
**[as-built]** `GD.Print` does **not** feed `/console`; `ClothingTestBed.ReportLoad`
|
||||
had to be added before load failures surfaced there at all.
|
||||
- Python for plain gates: system `python` (3.12 on this box; PIL yes, numpy no).
|
||||
`python3` is a broken Windows Store stub — **do not use it**. Only stdlib + PIL; check
|
||||
before importing, degrade gracefully.
|
||||
Reference in New Issue
Block a user