From 4d27109668907235df0cd10d2f45179f97eb4e8c Mon Sep 17 00:00:00 2001 From: Zwitschi Date: Tue, 10 Nov 2026 12:40:30 +0100 Subject: [PATCH] refactor(tools): group the sixty scripts by what they talk to One directory with sixty scripts stopped being navigable, and the grouping is also the sandbox boundary: Tools/unreal/ commandlets and the two map builders, with ue_common.py and ue_placement.py as the shared imports (they need the engine) Tools/blender/ builders, exporters, the FBX preset, the MPFB character pipeline, blender_screenshots (they need the Blender Flatpak) Tools/review/ the visual loop: render_screenshots.sh, judge_screenshot.py, hero_compare.py, build_comparison_md.py, e2e_asset_check.py, hero_iterate.sh, visual_loop.py, visual_targets.json Tools/gen/ image and texture generation: the ComfyUI driver, deterministic texture writers, reference fetching Tools/input/ OS-level input injection root _common.py, check-lfs.sh, addon_compat_probe.py, sibling_asset_audit.py, tests/ Every moved script now resolves the repository root as ../.. and puts the tools root plus its siblings on sys.path, so 'import ue_common' still works from unreal/. The test suite bootstrap does the same for the five folders. render_screenshots.sh takes the folder as a third argument to run_cmdlet, and the e2e driver, hero_iterate.sh and the comparison generator were repointed. 51 README rows now link to their new location, the README gains a Layout section naming each folder's sandbox, and the architecture notes and Blender README follow the moves. Verified: 108 tests pass, prettier is clean, judge_screenshot.py and hero_compare.py run from their new homes, and the e2e run was aborted mid-way (it had been rebuilding an empty DemoMap for 15 minutes) and is being rerun against the new layout. --- AGENTS.md | 8 +- Art/blender/README.md | 2 +- CHANGELOG.md | 42 +++--- Docs/Architecture/04_solution_strategy.md | 2 +- Docs/Architecture/05_building_block_view.md | 2 +- Docs/Architecture/06_runtime_view.md | 6 +- Docs/Architecture/10_quality_requirements.md | 4 +- Docs/build.md | 38 ++--- Docs/controls.md | 6 +- Docs/release-checks.md | 6 +- Docs/setup.md | 2 +- Game/GigLine/Content/Hollywood/DemoMap.umap | 4 +- .../Hollywood/Site/Kerb_North_00.uasset | 2 +- .../Hollywood/Site/Kerb_North_01.uasset | 2 +- .../Hollywood/Site/Kerb_North_02.uasset | 2 +- .../Hollywood/Site/Kerb_North_03.uasset | 2 +- .../Hollywood/Site/Kerb_North_04.uasset | 2 +- .../Hollywood/Site/Kerb_North_05.uasset | 2 +- .../Hollywood/Site/Kerb_North_06.uasset | 2 +- .../Hollywood/Site/Kerb_North_07.uasset | 2 +- .../Hollywood/Site/Kerb_North_08.uasset | 2 +- .../Hollywood/Site/Kerb_North_09.uasset | 2 +- .../Hollywood/Site/Kerb_North_10.uasset | 2 +- .../Hollywood/Site/Kerb_North_11.uasset | 2 +- .../Hollywood/Site/Kerb_North_12.uasset | 2 +- .../Hollywood/Site/Kerb_North_13.uasset | 2 +- .../Hollywood/Site/Kerb_South_00.uasset | 2 +- .../Hollywood/Site/Kerb_South_01.uasset | 2 +- .../Hollywood/Site/Kerb_South_02.uasset | 2 +- .../Hollywood/Site/Kerb_South_03.uasset | 2 +- .../Hollywood/Site/Kerb_South_04.uasset | 2 +- .../Hollywood/Site/Kerb_South_05.uasset | 2 +- .../Hollywood/Site/Kerb_South_06.uasset | 2 +- .../Hollywood/Site/Kerb_South_07.uasset | 2 +- .../Hollywood/Site/Kerb_South_08.uasset | 2 +- .../Hollywood/Site/Kerb_South_09.uasset | 2 +- .../Hollywood/Site/Kerb_South_10.uasset | 2 +- .../Hollywood/Site/Kerb_South_11.uasset | 2 +- .../Hollywood/Site/Kerb_South_12.uasset | 2 +- .../Hollywood/Site/Kerb_South_13.uasset | 2 +- .../Content/Hollywood/Site/M_Asphalt.uasset | 2 +- .../Content/Hollywood/Site/M_Concrete.uasset | 2 +- .../Hollywood/Site/M_FacadeBanner.uasset | 2 +- .../Hollywood/Site/M_FacadeGlass.uasset | 2 +- .../Hollywood/Site/M_FacadeStone.uasset | 2 +- .../Hollywood/Site/M_FacadeStoneShadow.uasset | 2 +- .../Content/Hollywood/Site/M_Ground.uasset | 2 +- .../Hollywood/Site/M_KerbConcrete.uasset | 2 +- .../Content/Hollywood/Site/M_RoadPaint.uasset | 2 +- .../Content/Hollywood/Site/M_Stucco.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_00.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_01.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_02.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_03.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_04.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_05.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_06.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_07.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_08.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_09.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_10.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_11.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_12.uasset | 2 +- .../Hollywood/Site/Sidewalk_North_13.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_00.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_01.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_02.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_03.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_04.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_05.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_06.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_07.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_08.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_09.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_10.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_11.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_12.uasset | 2 +- .../Hollywood/Site/Sidewalk_South_13.uasset | 2 +- .../Content/Hollywood/Site/Terrain_DEM.uasset | 2 +- .../Hollywood/Textures/T_Asphalt_BC.uasset | 2 +- .../Hollywood/Textures/T_Asphalt_R.uasset | 2 +- .../Hollywood/Textures/T_Concrete_BC.uasset | 2 +- .../Hollywood/Textures/T_Concrete_R.uasset | 2 +- .../Textures/T_StoneAshlar_BC.uasset | 2 +- .../Hollywood/Textures/T_StoneAshlar_R.uasset | 2 +- .../Hollywood/Textures/T_Stucco_BC.uasset | 2 +- .../Hollywood/Textures/T_Stucco_R.uasset | 2 +- Game/GigLine/Content/Test/ReviewMap.umap | 2 +- Game/GigLine/Content/Test/_ProbeLevel.umap | 3 + MEMORY.md | 64 ++++----- Screenshots/README.md | 17 ++- THIRD_PARTY_NOTICES.md | 6 +- TODO.md | 18 +-- Tools/README.md | 119 +++++++++------- Tools/{ => blender}/blender_screenshots.py | 4 +- Tools/{ => blender}/character_anim_build.py | 4 +- .../{ => blender}/character_clothing_build.py | 4 +- Tools/{ => blender}/character_hair_build.py | 6 +- Tools/{ => blender}/character_match.py | 4 +- .../comfyui_character_blueprint.py | 10 +- Tools/{ => blender}/export_block_frontage.py | 4 +- Tools/{ => blender}/export_north_buildings.py | 4 +- Tools/{ => blender}/facade_kit_build.py | 6 +- Tools/{ => blender}/fbx_export_preset.py | 0 Tools/{ => blender}/hero_facade_build.py | 8 +- Tools/{ => blender}/mpfb_character_export.py | 4 +- Tools/{ => blender}/retail_frontage_build.py | 6 +- Tools/{ => blender}/scale_test_export.py | 4 +- Tools/{ => blender}/theatre_trim_build.py | 4 +- Tools/{ => blender}/vehicle_build.py | 4 +- Tools/{ => blender}/vehicle_from_template.py | 4 +- Tools/{ => gen}/comfyui_generate.py | 8 +- Tools/{ => gen}/commons_fetch.py | 8 +- Tools/{ => gen}/facade_detail_concepts.py | 10 +- .../{ => gen}/generate_character_textures.py | 4 +- Tools/{ => gen}/generate_facade_textures.py | 6 +- Tools/{ => gen}/page_image_fetch.py | 4 +- Tools/{ => input}/input_emulation.py | 2 +- Tools/{ => input}/input_test.py | 10 +- Tools/{ => input}/run_pie.sh | 2 +- Tools/{ => review}/build_comparison_md.py | 6 +- Tools/review/e2e_asset_check.py | 130 ++++++++++++++++++ Tools/{ => review}/hero_compare.py | 2 +- Tools/{ => review}/hero_iterate.sh | 14 +- Tools/{ => review}/judge_screenshot.py | 8 +- Tools/{ => review}/render_screenshots.py | 2 +- Tools/{ => review}/render_screenshots.sh | 12 +- Tools/{ => review}/shadow_gap.py | 2 +- Tools/{ => review}/visual_loop.py | 12 +- Tools/{ => review}/visual_targets.json | 0 Tools/tests/test_block_height_report.py | 9 ++ Tools/tests/test_character_match.py | 9 ++ Tools/tests/test_comfyui_texture.py | 9 ++ Tools/tests/test_facade_detail_concepts.py | 9 ++ Tools/tests/test_fbx_export_preset.py | 9 ++ Tools/tests/test_input_emulation.py | 9 ++ Tools/tests/test_judge_combine.py | 9 ++ Tools/tests/test_mapbox_buildings.py | 9 ++ Tools/tests/test_retail_frontage.py | 9 ++ Tools/tests/test_shadow_gap.py | 9 ++ Tools/tests/test_ue_common.py | 9 ++ Tools/tests/test_ue_placement.py | 13 +- Tools/tests/test_visual_loop.py | 9 ++ Tools/{ => unreal}/ue_apply_facade_kit.py | 10 +- Tools/{ => unreal}/ue_audit_scene.py | 6 +- Tools/{ => unreal}/ue_build_demo_map.py | 45 +++--- Tools/{ => unreal}/ue_build_review_map.py | 31 ++++- .../{ => unreal}/ue_character_anim_import.py | 6 +- .../ue_character_clothing_import.py | 4 +- .../{ => unreal}/ue_character_hair_import.py | 4 +- Tools/{ => unreal}/ue_character_import.py | 2 +- Tools/{ => unreal}/ue_character_materials.py | 4 +- .../{ => unreal}/ue_clear_simple_collision.py | 2 +- Tools/{ => unreal}/ue_common.py | 2 +- Tools/{ => unreal}/ue_create_input_assets.py | 2 +- Tools/{ => unreal}/ue_hero_facade_import.py | 8 +- .../{ => unreal}/ue_import_block_frontage.py | 8 +- Tools/{ => unreal}/ue_import_facade_kit.py | 8 +- .../{ => unreal}/ue_import_north_buildings.py | 4 +- Tools/{ => unreal}/ue_import_retail_detail.py | 6 +- Tools/{ => unreal}/ue_import_site.py | 2 +- Tools/{ => unreal}/ue_placement.py | 2 +- Tools/unreal/ue_recreate_maps.py | 86 ++++++++++++ Tools/{ => unreal}/ue_scale_test_import.py | 2 +- Tools/{ => unreal}/ue_stage_theatre_trim.py | 6 +- Tools/{ => unreal}/ue_vehicle_import.py | 4 +- Tools/{ => unreal}/ue_vehicle_materials.py | 6 +- Tools/{ => unreal}/ue_vehicle_probe_map.py | 2 +- 168 files changed, 799 insertions(+), 407 deletions(-) create mode 100644 Game/GigLine/Content/Test/_ProbeLevel.umap rename Tools/{ => blender}/blender_screenshots.py (97%) rename Tools/{ => blender}/character_anim_build.py (99%) rename Tools/{ => blender}/character_clothing_build.py (98%) rename Tools/{ => blender}/character_hair_build.py (97%) rename Tools/{ => blender}/character_match.py (99%) rename Tools/{ => blender}/comfyui_character_blueprint.py (92%) rename Tools/{ => blender}/export_block_frontage.py (95%) rename Tools/{ => blender}/export_north_buildings.py (93%) rename Tools/{ => blender}/facade_kit_build.py (97%) rename Tools/{ => blender}/fbx_export_preset.py (100%) rename Tools/{ => blender}/hero_facade_build.py (99%) rename Tools/{ => blender}/mpfb_character_export.py (97%) rename Tools/{ => blender}/retail_frontage_build.py (98%) rename Tools/{ => blender}/scale_test_export.py (93%) rename Tools/{ => blender}/theatre_trim_build.py (97%) rename Tools/{ => blender}/vehicle_build.py (99%) rename Tools/{ => blender}/vehicle_from_template.py (99%) rename Tools/{ => gen}/comfyui_generate.py (98%) rename Tools/{ => gen}/commons_fetch.py (96%) rename Tools/{ => gen}/facade_detail_concepts.py (98%) rename Tools/{ => gen}/generate_character_textures.py (97%) rename Tools/{ => gen}/generate_facade_textures.py (97%) rename Tools/{ => gen}/page_image_fetch.py (97%) rename Tools/{ => input}/input_emulation.py (99%) rename Tools/{ => input}/input_test.py (92%) rename Tools/{ => input}/run_pie.sh (94%) rename Tools/{ => review}/build_comparison_md.py (96%) create mode 100644 Tools/review/e2e_asset_check.py rename Tools/{ => review}/hero_compare.py (97%) rename Tools/{ => review}/hero_iterate.sh (83%) rename Tools/{ => review}/judge_screenshot.py (97%) rename Tools/{ => review}/render_screenshots.py (98%) rename Tools/{ => review}/render_screenshots.sh (90%) rename Tools/{ => review}/shadow_gap.py (98%) rename Tools/{ => review}/visual_loop.py (97%) rename Tools/{ => review}/visual_targets.json (100%) rename Tools/{ => unreal}/ue_apply_facade_kit.py (96%) rename Tools/{ => unreal}/ue_audit_scene.py (93%) rename Tools/{ => unreal}/ue_build_demo_map.py (96%) rename Tools/{ => unreal}/ue_build_review_map.py (95%) rename Tools/{ => unreal}/ue_character_anim_import.py (92%) rename Tools/{ => unreal}/ue_character_clothing_import.py (92%) rename Tools/{ => unreal}/ue_character_hair_import.py (91%) rename Tools/{ => unreal}/ue_character_import.py (99%) rename Tools/{ => unreal}/ue_character_materials.py (97%) rename Tools/{ => unreal}/ue_clear_simple_collision.py (96%) rename Tools/{ => unreal}/ue_common.py (95%) rename Tools/{ => unreal}/ue_create_input_assets.py (98%) rename Tools/{ => unreal}/ue_hero_facade_import.py (97%) rename Tools/{ => unreal}/ue_import_block_frontage.py (88%) rename Tools/{ => unreal}/ue_import_facade_kit.py (91%) rename Tools/{ => unreal}/ue_import_north_buildings.py (96%) rename Tools/{ => unreal}/ue_import_retail_detail.py (91%) rename Tools/{ => unreal}/ue_import_site.py (98%) rename Tools/{ => unreal}/ue_placement.py (96%) create mode 100644 Tools/unreal/ue_recreate_maps.py rename Tools/{ => unreal}/ue_scale_test_import.py (95%) rename Tools/{ => unreal}/ue_stage_theatre_trim.py (95%) rename Tools/{ => unreal}/ue_vehicle_import.py (97%) rename Tools/{ => unreal}/ue_vehicle_materials.py (95%) rename Tools/{ => unreal}/ue_vehicle_probe_map.py (97%) diff --git a/AGENTS.md b/AGENTS.md index fc4cb88..c29dae0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,7 +126,7 @@ Rules that keep tools reusable and reviewable: - **Result files for Unreal.** Unreal commandlets swallow `print()`; write the outcome to `/tmp/gigline/.txt` and read that instead of trusting the exit code alone. - **Absolute paths into sandboxes.** Blender Flatpak (`LD_LIBRARY_PATH="$B/blender/lib"`) and Unreal do not share the repo's working directory; pass absolute paths. - **Clean worktree for generators.** Tools that rewrite committed assets (site/street/hero builders, importers, map builders) run from a clean worktree; inspect the LFS diff before committing. Note that `.blend` and `.umap` check out read-only — `chmod u+w` first or the save silently fails. -- **Network and paid-API tools are human-triggered.** `Tools/commons_fetch.py`, `Tools/page_image_fetch.py`, `Tools/judge_screenshot.py` (OpenRouter) and anything similar never sit in a gate or CI. Handle HTTP 429 with back-off rather than retrying immediately. +- **Network and paid-API tools are human-triggered.** `Tools/gen/commons_fetch.py`, `Tools/gen/page_image_fetch.py`, `Tools/review/judge_screenshot.py` (OpenRouter) and anything similar never sit in a gate or CI. Handle HTTP 429 with back-off rather than retrying immediately. - **Measure, don't assert.** Verification tools print the sampled numbers — counts, holes, Z ranges, worst step and its location, frame-time percentiles — so a claim in `TODO.md`, `CHANGELOG.md` or `Docs/` can be re-checked by re-running the command. **Document every tool.** A new or changed tool gets a row in the inventory table of the README beside it: [`Tools/README.md`](Tools/README.md), [`Geo/README.md`](Geo/README.md), or [`Art/blender/README.md`](Art/blender/README.md). What it does, its arguments, and a typical command. A tool without a README row is unfinished. @@ -212,10 +212,10 @@ Do not copy the common packs into the repository: a second copy under a differen Any change that affects the game's appearance — meshes, materials, lighting, levels, character, camera, post-processing — must be rendered for human review: -1. Run `Tools/render_screenshots.sh` (rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it, captures the fixed camera list, copies the PNGs into `Screenshots/`). -2. Optional pre-check with a vision model: `Tools/judge_screenshot.py --image --expect ""` (or `--shots` to run every shot in the README) flags likely-missing content before a human looks. It reads `OPENROUTER_API_KEY` from `.env` (ignored). Small helper only — it never replaces the human verdict. +1. Run `Tools/review/render_screenshots.sh` (rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it, captures the fixed camera list, copies the PNGs into `Screenshots/`). +2. Optional pre-check with a vision model: `Tools/review/judge_screenshot.py --image --expect ""` (or `--shots` to run every shot in the README) flags likely-missing content before a human looks. It reads `OPENROUTER_API_KEY` from `.env` (ignored). Small helper only — it never replaces the human verdict. 3. Update `Screenshots/README.md` **in the same change as any visual work** — every change that affects appearance, not only new shots: the image is **embedded inline** (`![alt](demo/.png)`) with its view, expected content, verification and approval state, plus one row in the status table at the top. The review happens in the file; a path alone is not a review, and a shot whose README section is stale is a defect. 4. The PNGs **are committed** so reviewers see exactly what the build rendered (`.gitignore` does not exclude them). Keep them in sync with this README: drop retired shots in the same change. 5. Wait for the user's approval before treating a visual change as done; record approval by ticking both the shot's section and its row in the status table in `Screenshots/README.md`. -New content that needs a different camera angle gets a new entry in the shot list in `Tools/ue_build_review_map.py`. +New content that needs a different camera angle gets a new entry in the shot list in `Tools/unreal/ue_build_review_map.py`. diff --git a/Art/blender/README.md b/Art/blender/README.md index 6cb4ba0..56c61a4 100644 --- a/Art/blender/README.md +++ b/Art/blender/README.md @@ -153,7 +153,7 @@ The extract is complete for the block but not for every relation inside its bbox ## Scale test and the Unreal frame (2026-11-07) -`Art/blender/scaletest/scale-test-2026-11-07.blend` — three 1 m cubes at origin, +10 m east, +10 m north, plus one red test material. Regenerate with `Tools/scale_test_export.py`; import with `Tools/ue_scale_test_import.py`. Both are the named-preset templates for production assets. +`Art/blender/scaletest/scale-test-2026-11-07.blend` — three 1 m cubes at origin, +10 m east, +10 m north, plus one red test material. Regenerate with `Tools/blender/scale_test_export.py`; import with `Tools/unreal/ue_scale_test_import.py`. Both are the named-preset templates for production assets. Export preset **`ue_metric_v1`** (Blender FBX): `apply_unit_scale`, `apply_scale_options='FBX_SCALE_NONE'`, `axis_forward='-Y'`, `axis_up='Z'`, `use_space_transform`, no bake-space-transform. Import preset **`ue_metric_v1`** (UE `FbxImportUI`): static mesh, import materials, uniform scale 1.0, no unit conversion, no force-front-X. diff --git a/CHANGELOG.md b/CHANGELOG.md index a188ffc..f45b248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,12 +38,12 @@ The character becomes a character: clothed, haired, and measured against a gener ### Added -- **Clothing** — navy tee, dark jeans and off-white shoes as three separate skinned meshes (`Tools/character_clothing_build.py`), built from the body's own faces per the `character-artist` workflow: duplicate faces, solidify for cloth thickness, one mesh per piece. Each imports with its own single material slot and is attached as a leader-pose component. -- **Hair cards** (`Tools/character_hair_build.py`) — a scalp cap plus 26 cards in three overlapping rows, quads only, every vertex weighted to `head`. Cards are oriented from the scalp normal at their anchor, so the cut follows the skull instead of standing out as spikes. -- **Character blueprints** (`Tools/comfyui_character_blueprint.py`) — front, side, back and head views generated by the local ComfyUI instance from one description, with fixed seeds and a JSON record of prompts and prompt ids. -- **Blueprint comparison** (`Tools/character_match.py`) — figure aspect, fill and coverage deltas, mean CIE76 and dominant-palette distance, plus a side-by-side montage. The deltas stay separate on purpose: one similarity number between a photoreal sheet and a low-detail body would mean nothing. -- **ComfyUI driver** (`Tools/comfyui_generate.py`) and **procedural textures** (`Tools/generate_character_textures.py`), each writing a provenance sidecar. -- **Material builder** (`Tools/ue_character_materials.py`) — `M_Character_Skin` and `M_Character_Hair_Mat` built from scratch, because `unreal.MaterialEditingLibrary` segfaults in a headless commandlet when it rebuilds an existing graph. +- **Clothing** — navy tee, dark jeans and off-white shoes as three separate skinned meshes (`Tools/blender/character_clothing_build.py`), built from the body's own faces per the `character-artist` workflow: duplicate faces, solidify for cloth thickness, one mesh per piece. Each imports with its own single material slot and is attached as a leader-pose component. +- **Hair cards** (`Tools/blender/character_hair_build.py`) — a scalp cap plus 26 cards in three overlapping rows, quads only, every vertex weighted to `head`. Cards are oriented from the scalp normal at their anchor, so the cut follows the skull instead of standing out as spikes. +- **Character blueprints** (`Tools/blender/comfyui_character_blueprint.py`) — front, side, back and head views generated by the local ComfyUI instance from one description, with fixed seeds and a JSON record of prompts and prompt ids. +- **Blueprint comparison** (`Tools/blender/character_match.py`) — figure aspect, fill and coverage deltas, mean CIE76 and dominant-palette distance, plus a side-by-side montage. The deltas stay separate on purpose: one similarity number between a photoreal sheet and a low-detail body would mean nothing. +- **ComfyUI driver** (`Tools/gen/comfyui_generate.py`) and **procedural textures** (`Tools/gen/generate_character_textures.py`), each writing a provenance sidecar. +- **Material builder** (`Tools/unreal/ue_character_materials.py`) — `M_Character_Skin` and `M_Character_Hair_Mat` built from scratch, because `unreal.MaterialEditingLibrary` segfaults in a headless commandlet when it rebuilds an existing graph. ### Changed @@ -92,7 +92,7 @@ Third-person body. The pawn gains a visible, animated body and a third-person ca ### Added -- **`Tools/character_anim_build.py`**: `GigLine_Idle` (60 frames) and `GigLine_Walk` (30 frames) at 30 fps, authored as parametric cycles so the loop closes by construction. Limb axes were **measured** on the rig — a probe rotated `upperleg01.L` by 0.5 rad on each axis and read `foot.L`, which showed X swings the limb fore/aft while Y twists and Z abducts. +- **`Tools/blender/character_anim_build.py`**: `GigLine_Idle` (60 frames) and `GigLine_Walk` (30 frames) at 30 fps, authored as parametric cycles so the loop closes by construction. Limb axes were **measured** on the rig — a probe rotated `upperleg01.L` by 0.5 rad on each axis and read `foot.L`, which showed X swings the limb fore/aft while Y twists and Z abducts. - **Third-person rig** (`ADR-9`): a visible skeletal body, a `USpringArmComponent` with collision testing, and a **V** toggle. One pawn carries both rigs; first person stays the shipped default. - **Walk capture harness**: a per-shot `ShotFollow` mode that re-places the review camera every frame in the pawn's frame, plus `SetReviewWalk` / `ReturnToStartForReview`, so a stride is captured deterministically and the fixed-camera shots still find the pawn at spawn. - **Review shots 6, 7 and 8** — third-person back, front and mid-stride — embedded in `Screenshots/README.md`. @@ -130,8 +130,8 @@ Retail frontage. The two retail buildings beside the hero are modelled from thei ### Added -- **`Tools/retail_frontage_build.py`**: 6800 Lee Drug and 6806–6820 built from the one usable reference (`Art/reference/retail-neighbours/`, Gb321, CC BY-SA 4.0). Lee Drug gets a bronze cornice, storefront glazing with mullions, framed portrait panels, a painted lettering band, two-tier parapet with reveals and a fluted corner tower; 6806–6820 gets a burgundy fascia, brown pilasters, white-framed windows with security bars and its own banded parapet. Built in world coordinates from the blockout bounds (Lee Drug X −33.20…−14.37, face Y −14.63, H 12.10 m; 6806–6820 X −66.08…−33.07), joined to one mesh per building with a JSON part report. -- **`Tools/ue_import_retail_detail.py`**: imports the two meshes with complex-as-simple collision — trim decorates a solid the player already collides with and must not add snag points. +- **`Tools/blender/retail_frontage_build.py`**: 6800 Lee Drug and 6806–6820 built from the one usable reference (`Art/reference/retail-neighbours/`, Gb321, CC BY-SA 4.0). Lee Drug gets a bronze cornice, storefront glazing with mullions, framed portrait panels, a painted lettering band, two-tier parapet with reveals and a fluted corner tower; 6806–6820 gets a burgundy fascia, brown pilasters, white-framed windows with security bars and its own banded parapet. Built in world coordinates from the blockout bounds (Lee Drug X −33.20…−14.37, face Y −14.63, H 12.10 m; 6806–6820 X −66.08…−33.07), joined to one mesh per building with a JSON part report. +- **`Tools/unreal/ue_import_retail_detail.py`**: imports the two meshes with complex-as-simple collision — trim decorates a solid the player already collides with and must not add snag points. - **`Art/blender/retail/ref-analysis.md`**: the reference analysis the `references` skill pack requires, written before modelling. - **`Tools/_common.py`** and `Tools/tests/test_retail_frontage.py`: the bay-layout arithmetic, extracted so it is testable without Blender (4 tests). - **Review shot 5**, `demo/review-demo-retail-frontage`, embedded in `Screenshots/README.md`. @@ -144,7 +144,7 @@ Retail frontage. The two retail buildings beside the hero are modelled from thei ### Fixed -- **`Tools/render_screenshots.sh` never captured anything.** The capture step called `./Engine/...` while every commandlet above it went through `run_cmdlet`'s `cd "$ENGINE"`. Under `set -e` the script died at exit 127 _after_ rebuilding the maps, so the PNGs silently stayed a day old and `--verify` never ran. +- **`Tools/review/render_screenshots.sh` never captured anything.** The capture step called `./Engine/...` while every commandlet above it went through `run_cmdlet`'s `cd "$ENGINE"`. Under `set -e` the script died at exit 127 _after_ rebuilding the maps, so the PNGs silently stayed a day old and `--verify` never ran. - **Duplicate `RetailTrim_*` actors on every rebuild**: the demo builder's clear list did not include the new label, so each run stacked another copy. Caught by the scene audit, not by eye. - **Mural plates were placed 0.20 m inside the solid blockout volume** and never rendered; the inset is now faked with a proud frame and a near-flush plate. - **Trim detail sat 0.05 m off the wall**, under a pixel of depth at 40 m, so the wall won the depth test. Trim planes now stand 0.10–0.20 m proud. @@ -170,8 +170,8 @@ Façade finishing. The kit gets the hero detail it was missing, and a class of s ### Added -- **Scene audit** (`Tools/ue_audit_scene.py`): fails when any staged mesh has no material, when a frontage building is more than ±1 m off its verified footprint, or when two actors share a label. Wired into `Tools/render_screenshots.sh`, which now runs the whole staging chain in the correct order and stops on an audit failure. -- **`Tools/ue_placement.py`**: one source of truth for staged positions and the verified frontage spans, with `span_error` / `describe_span_failure` validators. +- **Scene audit** (`Tools/unreal/ue_audit_scene.py`): fails when any staged mesh has no material, when a frontage building is more than ±1 m off its verified footprint, or when two actors share a label. Wired into `Tools/review/render_screenshots.sh`, which now runs the whole staging chain in the correct order and stops on an audit failure. +- **`Tools/unreal/ue_placement.py`**: one source of truth for staged positions and the verified frontage spans, with `span_error` / `describe_span_failure` validators. - **Hero fluting**: 20 grooves per column as shallow shadow-tone cylinders (120 pieces) rather than boolean cuts. - **Presence of references for the retail run**: `Art/reference/retail-neighbours/` (6800 Lee Drug and its neighbour). Local only, not committed. @@ -210,9 +210,9 @@ Building façades. The block goes from flat extruded boxes with flat colours to - **Generated surface textures** — deterministic stdlib PNGs for ashlar stone, stucco, asphalt and concrete (base colour + roughness), sampled world-aligned with per-material tiling; byte-identical on re-run. - **Materials**: masonry coursing and roughness variation in the material graphs; `M_FacadeStoneShadow`, `M_FacadeGlass` (specular 0.62 ≈ IOR 1.5) and `M_FacadeBanner`. Material assignment is now **per slot**, keyed on the imported slot material, instead of slot 0 only. - **El Capitan Theatre trim**: projecting marquee (12.0 × 2.60 m canopy at a 4.60 m soffit), illuminated fascia, recessed soffit, and a vertical blade sign (1.30 × 5.20 m) with frame and wall bracket. -- **Visual review loop** (`Tools/visual_loop.py` + `visual_targets.json`): verifies every capture against an expectation (missing, blank, tone-less or wrong-size shots fail) and measures each render against its reference photograph, emitting feedback that names the discipline to consult. Iterations are written under `Art/reference/iterations/`, which is gitignored until a shot is confirmed. -- **`Tools/fbx_export_preset.py`** — one export preset replacing six copies, and it exports smoothing groups (`FACE`), so round geometry is no longer faceted. -- **`Tools/ue_common.py`** — clears the read-only bit an LFS `.umap` checks out with and makes a refused save loud. +- **Visual review loop** (`Tools/review/visual_loop.py` + `visual_targets.json`): verifies every capture against an expectation (missing, blank, tone-less or wrong-size shots fail) and measures each render against its reference photograph, emitting feedback that names the discipline to consult. Iterations are written under `Art/reference/iterations/`, which is gitignored until a shot is confirmed. +- **`Tools/blender/fbx_export_preset.py`** — one export preset replacing six copies, and it exports smoothing groups (`FACE`), so round geometry is no longer faceted. +- **`Tools/unreal/ue_common.py`** — clears the read-only bit an LFS `.umap` checks out with and makes a refused save loud. ### Changed @@ -249,7 +249,7 @@ Packaging and setup fixes. No gameplay or visual scope; this patch makes the pip ### Fixed -- **Map imports could report success while saving nothing.** `.umap` files are LFS-tracked and check out read-only, so `save_map()` returned `False` with no error and the staged actor vanished on the next load. New `Tools/ue_common.py` (`make_writable`, `require_saved`) clears the read-only bit and makes the failure loud; `ue_hero_facade_import.py` and `ue_import_north_buildings.py` now use it. Verified from a forced `0o444` map: chmod logged, `save_map: True`, exit 0. +- **Map imports could report success while saving nothing.** `.umap` files are LFS-tracked and check out read-only, so `save_map()` returned `False` with no error and the staged actor vanished on the next load. New `Tools/unreal/ue_common.py` (`make_writable`, `require_saved`) clears the read-only bit and makes the failure loud; `ue_hero_facade_import.py` and `ue_import_north_buildings.py` now use it. Verified from a forced `0o444` map: chmod logged, `save_map: True`, exit 0. ### Changed @@ -268,8 +268,8 @@ Scale and façade correction patch. Buildings and the hero elevation now match t ### Added -- **`Tools/facade_detail_concepts.py`** — generates frontal building plates (whole elevation, entrance in its façade, building with neighbours) from a local reference photograph through OpenRouter image models. Spec-driven, never overwrites, writes a provenance sidecar per image, output stays in the gitignored `Art/reference/` tree. Local working material only. -- **`Tools/fbx_export_preset.py`** — the single FBX export preset (`ue_metric_v1` / `ue_character_v1`). Replaces the argument block that had been copied into six exporters. +- **`Tools/gen/facade_detail_concepts.py`** — generates frontal building plates (whole elevation, entrance in its façade, building with neighbours) from a local reference photograph through OpenRouter image models. Spec-driven, never overwrites, writes a provenance sidecar per image, output stays in the gitignored `Art/reference/` tree. Local working material only. +- **`Tools/blender/fbx_export_preset.py`** — the single FBX export preset (`ue_metric_v1` / `ue_character_v1`). Replaces the argument block that had been copied into six exporters. - **Façade occlusion probe** in `-qaselftest`: 40 m traces sideways from the road centre at walking height, required wherever a building is staged. - **Ionic detail on the hero**: base moulding at every shaft foot, a square abacus over every capital, and a 52-dentil cornice course. - **Tool tests**: `Tools/tests/` now covers the input driver, height classification, the Mapbox cross-check, the concept generator and the FBX preset (49 tests). @@ -335,16 +335,16 @@ The first playable release: a walkable exterior demo of the **6840 Hollywood Blv - **Unreal project** (`Game/GigLine`) — C++ game module, Enhanced Input, packaged Linux build, and a `DemoMap` that a plain launch opens directly. - **Walkable demo level** — terrain from the 3DEP DEM, Hollywood Blvd carriageway, both sidewalks and kerbs with collision, the designated mid-block crossing, and fixed daytime lighting (sun, sky light, sky atmosphere, height fog, pinned exposure). -- **Hero façade** for 6840 Hollywood Blvd — six-column neoclassical elevation with three genuine portal recesses, entablature, parapet and motto band (`Tools/hero_facade_build.py`, `Tools/ue_hero_facade_import.py`). +- **Hero façade** for 6840 Hollywood Blvd — six-column neoclassical elevation with three genuine portal recesses, entablature, parapet and motto band (`Tools/blender/hero_facade_build.py`, `Tools/unreal/ue_hero_facade_import.py`). - **Block frontage** — the five verified south-side neighbours (6922, 6902, 6838 El Capitan Theatre, 6822 Budget, 6806–6822, 6800 Lee Drug) and the two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) placed at their OSM positions. - **First-person player** — `AGigLineCharacter`: 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look, jump, reset (`R`) and pause (`Escape`); controls documented in `Docs/controls.md` and shown in-game. - **In-game ODbL attribution** — `AGigLineHUD` draws the OpenStreetMap credit and licence line every frame. -- **QA harness** — `-qaselftest` (walk, reset, pause, seam transect, invisible-wall probe, frame-time capture) and `Tools/input_test.py` / `Tools/input_emulation.py` for OS-level virtual keyboard/mouse input. +- **QA harness** — `-qaselftest` (walk, reset, pause, seam transect, invisible-wall probe, frame-time capture) and `Tools/input/input_test.py` / `Tools/input/input_emulation.py` for OS-level virtual keyboard/mouse input. - **Documentation** — setup, build, toolchain, licensing, ODbL review, release checks, and arc42 architecture docs. ### Fixed -- **All controls were dead in packaged and PIE builds.** The mapping-context generator wrote key bindings to the deprecated `mappings` array; UE 5.8 reads `DefaultKeyMappings.Mappings`, so the cooked game had zero runtime mappings and every key resolved to nothing. `Tools/ue_create_input_assets.py` now populates the current field. +- **All controls were dead in packaged and PIE builds.** The mapping-context generator wrote key bindings to the deprecated `mappings` array; UE 5.8 reads `DefaultKeyMappings.Mappings`, so the cooked game had zero runtime mappings and every key resolved to nothing. `Tools/unreal/ue_create_input_assets.py` now populates the current field. - **"Fall-through" was an 8 m invisible box.** `Terrain_DEM` shipped only a bounding-box collision proxy; forcing complex-as-simple collision (`UGigLineAssetTools`) put the player on the actual street. - **Reset returned the player to mid-air** — the start position is now captured on the first grounded frame. - **Plain launch opened the engine template** — the default map now lives in `DefaultEngine.ini` and points at `DemoMap`. diff --git a/Docs/Architecture/04_solution_strategy.md b/Docs/Architecture/04_solution_strategy.md index 7f7d693..d384c09 100644 --- a/Docs/Architecture/04_solution_strategy.md +++ b/Docs/Architecture/04_solution_strategy.md @@ -30,7 +30,7 @@ All modelling and geographic processing live outside the game project in `Art/`, ## Quality achievement -- **Realism first**: reference photography and measured data drive every façade decision; renders are compared against the photographs with `Tools/visual_loop.py`, and a façade is built from the shared 4.96 m module kit so the street wall reads consistently. Materials are authored in Unreal with world-aligned generated textures, and lighting is fixed daytime so a shot is comparable between runs. +- **Realism first**: reference photography and measured data drive every façade decision; renders are compared against the photographs with `Tools/review/visual_loop.py`, and a façade is built from the shared 4.96 m module kit so the street wall reads consistently. Materials are authored in Unreal with world-aligned generated textures, and lighting is fixed daytime so a shot is comparable between runs. - **Performance as a budget, not a driver**: dynamic-only lighting (no build step, no static shadow maps), world-space-noise materials (cheap procedural variation), minimal overdraw for street-level geometry, and a 60 FPS at 1440p floor that every release must meet. It closes the quality list because it constrains how much realism fits, not what the block should look like. - **Walkability**: complex-as-simple collision on terrain (C++ helper forces the flag on the asset), block-all collision on every site mesh, swept transect QA verifies zero raycast holes. diff --git a/Docs/Architecture/05_building_block_view.md b/Docs/Architecture/05_building_block_view.md index 5ced4bb..a0e4945 100644 --- a/Docs/Architecture/05_building_block_view.md +++ b/Docs/Architecture/05_building_block_view.md @@ -49,7 +49,7 @@ Static meshes and materials in `/Game/Hollywood/Site/`: | `SM_HeroFacade` | El Capitan Entertainment Centre detailed façade | | `SM_ElCapitanTheatre`, `SM_Budget_6822`, `SM_Hollywood_6806`, `SM_Hollywood_6902`, `SM_Hollywood_6922`, `SM_LeeDrug_6800` | South block frontage (6 buildings) | | `SM_Madame_Tussauds`, `SM_TCL_Chinese_Theatre` | North-side buildings (within 50 m) | -| `SM_Retail_LeeDrug`, `SM_Retail_6806` | Retail trim over 6800 and 6806–6820, modelled from their own photograph (`Tools/retail_frontage_build.py`, staged as `RetailTrim_*`). `6822 Budget` stays a bare volume: no photograph exists. | +| `SM_Retail_LeeDrug`, `SM_Retail_6806` | Retail trim over 6800 and 6806–6820, modelled from their own photograph (`Tools/blender/retail_frontage_build.py`, staged as `RetailTrim_*`). `6822 Budget` stays a bare volume: no photograph exists. | ### Art Pipeline diff --git a/Docs/Architecture/06_runtime_view.md b/Docs/Architecture/06_runtime_view.md index 06b7745..ae8cbd2 100644 --- a/Docs/Architecture/06_runtime_view.md +++ b/Docs/Architecture/06_runtime_view.md @@ -18,7 +18,7 @@ ## Screenshot capture -1. `Tools/render_screenshots.sh` compiles editor, builds `ReviewMap` from `Tools/ue_build_review_map.py`, launches game in `-game` mode. +1. `Tools/review/render_screenshots.sh` compiles editor, builds `ReviewMap` from `Tools/unreal/ue_build_review_map.py`, launches game in `-game` mode. 2. `AGigLineShotActor` iterates the camera list, sets view, calls `HighResShot 1280x720`, waits for capture. @@ -48,6 +48,6 @@ 1. Blender headless (`Tools/export_*.py`) reads `.blend` files, applies metre→centimetre conversion, exports FBX with `ue_metric_v1` preset. -2. Unreal headless (`Tools/ue_import_*.py`) imports FBX via Interchange, places meshes at geographic coordinates, sets collision profiles. +2. Unreal headless (`Tools/unreal/ue_import_*.py`) imports FBX via Interchange, places meshes at geographic coordinates, sets collision profiles. -3. `Tools/ue_build_demo_map.py` assembles `DemoMap`: creates/repositions actors, sets lighting, saves map and assets. +3. `Tools/unreal/ue_build_demo_map.py` assembles `DemoMap`: creates/repositions actors, sets lighting, saves map and assets. diff --git a/Docs/Architecture/10_quality_requirements.md b/Docs/Architecture/10_quality_requirements.md index 718e4bf..f4491c6 100644 --- a/Docs/Architecture/10_quality_requirements.md +++ b/Docs/Architecture/10_quality_requirements.md @@ -22,7 +22,7 @@ Ordered by priority: realism first, performance last. The order decides design t | ID | Source/Stimulus | Response | Metric | | --- | --- | --- | --- | -| QS-1 | Screenshot is compared against its reference photograph | The render reads as the same building | Similarity score and visual match confirmed by the owner; `Tools/visual_loop.py` reports the deltas | +| QS-1 | Screenshot is compared against its reference photograph | The render reads as the same building | Similarity score and visual match confirmed by the owner; `Tools/review/visual_loop.py` reports the deltas | | QS-2 | A façade is placed from measured data | It sits on its verified footprint | World span probed against `Geo/verification/` within ~1 m | | QS-3 | Player walks both sides of the demo | No fall-through, character stays at ground Z | 0 raycast holes in 84-sample transect | | QS-4 | Player holds W for 2 s on flat road | Character moves forward | ≥ 200 cm displacement, Z constant within ±5 cm | @@ -41,5 +41,5 @@ Ordered by priority: realism first, performance last. The order decides design t | BR-1 | A building with a reference photograph has a review shot compared against it | Silhouette and proportion deltas recorded per building in `Screenshots/README.md`; owner approval per shot | | BR-2 | Every building is either photo-matched or explicitly recorded as an unfounded blockout | List in `TODO.md`; no building in an in-between state | | BR-3 | Every building has a collision proxy and at least one LOD | Count of buildings missing either = 0 | -| BR-4 | No surface carries a placeholder material | Material audit in `Tools/ue_audit_scene.py` reports 0 placeholder slots | +| BR-4 | No surface carries a placeholder material | Material audit in `Tools/unreal/ue_audit_scene.py` reports 0 placeholder slots | | BR-5 | The realism pass does not break the floor | QS-9 still holds at 1440p, with the measured cost of the pass recorded | diff --git a/Docs/build.md b/Docs/build.md index e6dd2da..ded0b43 100644 --- a/Docs/build.md +++ b/Docs/build.md @@ -56,9 +56,9 @@ Both content-acceptance tests run headless and are the template for all asset im ```bash # Static-mesh scale test (Blender export -> Unreal import -> measure) -# Blender: Tools/scale_test_export.py, Unreal: Tools/ue_scale_test_import.py +# Blender: Tools/blender/scale_test_export.py, Unreal: Tools/unreal/ue_scale_test_import.py # MPFB character (generate -> rig -> bake -> export -> skeletal import) -# Blender: Tools/mpfb_character_export.py, Unreal: Tools/ue_character_import.py +# Blender: Tools/blender/mpfb_character_export.py, Unreal: Tools/unreal/ue_character_import.py # Unreal side of any test: ./Engine/Binaries/Linux/UnrealEditor-Cmd \ @@ -78,22 +78,22 @@ The hero block and the demo level are built by a fixed sequence. Each step write | Street | `Art/blender/street/street_mesh.py` | `Art/blender/street/street-*.blend` | | Site assembly | `Art/blender/site/site_assembly.py` | `Art/blender/site/site-*.blend` | | Site export | `Art/blender/site/export_site_fbx.py` | seven FBXs, one per mesh (terrain, road, both sidewalks, both kerbs, crossing) | -| Hero façade | `Tools/hero_facade_build.py` | `Art/blender/hero/hero-facade-*.blend` + JSON part report | -| Block frontage | `Tools/export_block_frontage.py` | six frontage FBXs from the blockout | -| North buildings | `Tools/export_north_buildings.py` | TCL and Tussaud's FBXs from the blockout | -| Site import | `Tools/ue_import_site.py` | `/Game/Hollywood/Site/*` | -| Frontage import | `Tools/ue_import_block_frontage.py` | `/Game/Hollywood/Frontage/*` | -| North import | `Tools/ue_import_north_buildings.py` | `/Game/Hollywood/North/*` | -| Hero import | `Tools/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` | -| Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` | -| Façade kit assembly | `Tools/ue_apply_facade_kit.py` | kit actors in `DemoMap` | -| Theatre trim | `Tools/ue_stage_theatre_trim.py` | marquee + blade sign actors | -| Scene audit | `Tools/ue_audit_scene.py` | materials, placement spans, labels (gate) | -| Review shots | `Tools/ue_build_review_map.py` + `Tools/render_screenshots.sh` | `Screenshots/` | +| Hero façade | `Tools/blender/hero_facade_build.py` | `Art/blender/hero/hero-facade-*.blend` + JSON part report | +| Block frontage | `Tools/blender/export_block_frontage.py` | six frontage FBXs from the blockout | +| North buildings | `Tools/blender/export_north_buildings.py` | TCL and Tussaud's FBXs from the blockout | +| Site import | `Tools/unreal/ue_import_site.py` | `/Game/Hollywood/Site/*` | +| Frontage import | `Tools/unreal/ue_import_block_frontage.py` | `/Game/Hollywood/Frontage/*` | +| North import | `Tools/unreal/ue_import_north_buildings.py` | `/Game/Hollywood/North/*` | +| Hero import | `Tools/unreal/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` | +| Demo map | `Tools/unreal/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` | +| Façade kit assembly | `Tools/unreal/ue_apply_facade_kit.py` | kit actors in `DemoMap` | +| Theatre trim | `Tools/unreal/ue_stage_theatre_trim.py` | marquee + blade sign actors | +| Scene audit | `Tools/unreal/ue_audit_scene.py` | materials, placement spans, labels (gate) | +| Review shots | `Tools/unreal/ue_build_review_map.py` + `Tools/review/render_screenshots.sh` | `Screenshots/` | -**Order matters.** `ue_build_demo_map.py` rebuilds the site materials, so any actor staged by another tool must be (re)staged _after_ it, or it keeps a reference to a material that no longer exists — that is how 604 actors once ended up untethered from their materials while the map only looked slightly flat. `Tools/render_screenshots.sh` runs the full chain in the right order and gates on `ue_audit_scene.py`. +**Order matters.** `ue_build_demo_map.py` rebuilds the site materials, so any actor staged by another tool must be (re)staged _after_ it, or it keeps a reference to a material that no longer exists — that is how 604 actors once ended up untethered from their materials while the map only looked slightly flat. `Tools/review/render_screenshots.sh` runs the full chain in the right order and gates on `ue_audit_scene.py`. -Every Blender exporter calls [`Tools/fbx_export_preset.py`](../Tools/fbx_export_preset.py) (`ue_metric_v1` / `ue_character_v1`) instead of repeating the FBX argument block — one place for the scale conversion, the axis mapping and `mesh_smooth_type="FACE"` (without which every Unreal import warns about missing smoothing groups and round geometry arrives faceted). +Every Blender exporter calls [`Tools/blender/fbx_export_preset.py`](../Tools/blender/fbx_export_preset.py) (`ue_metric_v1` / `ue_character_v1`) instead of repeating the FBX argument block — one place for the scale conversion, the axis mapping and `mesh_smooth_type="FACE"` (without which every Unreal import warns about missing smoothing groups and round geometry arrives faceted). Two rules that the pipeline enforces rather than documents: @@ -103,7 +103,7 @@ Two rules that the pipeline enforces rather than documents: ## Known engine quirks (2026-11-07) - One handled `PSOPrecache` ensure fires at packaged-build exit only; not blocking. -- Interchange 5.8 does not create material/texture assets for skeletal FBX imports — `Tools/ue_character_import.py` finishes that chain explicitly. +- Interchange 5.8 does not create material/texture assets for skeletal FBX imports — `Tools/unreal/ue_character_import.py` finishes that chain explicitly. - Android Turnkey status errors and datarouter DNS warnings in logs are benign. ## Packaged Linux build @@ -129,13 +129,13 @@ cd /run/media/system/DATA/Builds/GigLineQA/Linux | `Add-on not loaded: "bl_ext.blender_org.mpfb"` | `mpfb_character_export.py` needs the MPFB add-on visible to Blender | export `BLENDER_USER_SCRIPTS` (see the invocation in [../Art/blender/README.md](../Art/blender/README.md)) | | The packaged run exits after a couple of seconds during QA | `GigLineShotActor` runs its screenshot list and quits (`bQuitWhenDone`) | it skips its rounds when `-qaselftest` is on the command line — keep that guard | | Headless `-nullrhi` traces report no hits for everything | the null RHI has no physics scene | verify traces in the **packaged** run (`-qaselftest`), never in a commandlet | -| Import logs "No smoothing group information" for every mesh | `mesh_smooth_type="OFF"` in the FBX export | use `Tools/fbx_export_preset.py` (`FACE`) | +| Import logs "No smoothing group information" for every mesh | `mesh_smooth_type="OFF"` in the FBX export | use `Tools/blender/fbx_export_preset.py` (`FACE`) | ## PIE input test (compare editor vs packaged input intake) Use to isolate whether the "no controls respond" bug is the packaged standalone's input intake or the Enhanced Input wiring generally. -- `Tools/run_pie.sh` opens the editor on `/Game/Hollywood/DemoMap`. Press the green **Play** (PIE) button and walk / mouse-look. +- `Tools/input/run_pie.sh` opens the editor on `/Game/Hollywood/DemoMap`. Press the green **Play** (PIE) button and walk / mouse-look. - Interpretation: - PIE **works** -> the Enhanced Input wiring is fine and the standalone packaged build's SDL input intake is the environment-dependent bug. - PIE **fails** too -> the Enhanced Input setup itself is broken; rebuild the input config / character from scratch. diff --git a/Docs/controls.md b/Docs/controls.md index d17b4e8..fe71785 100644 --- a/Docs/controls.md +++ b/Docs/controls.md @@ -19,7 +19,7 @@ Keyboard and mouse. The same list is shown in-game, bottom-right of the frame (` - Movement is relative to where you are looking (controller yaw), so the capsule stays upright on the sloped street. - The mouse is captured on launch, so looking works without clicking first. `Escape` toggles the capture as part of pause. -- Bindings are Enhanced Input actions in `/Game/Input` built by `Tools/ue_create_input_assets.py`; the mapping context is `IMC_Default`. +- Bindings are Enhanced Input actions in `/Game/Input` built by `Tools/unreal/ue_create_input_assets.py`; the mapping context is `IMC_Default`. ## Changes since `0.1.0` @@ -48,10 +48,10 @@ The walking shot is driven per shot rather than by the switch: the shot actor en ## Testing input without a physical keyboard -`Tools/input_test.py` injects OS-level keyboard/mouse events through a virtual `/dev/uinput` device (see `Tools/input_emulation.py`), e.g.: +`Tools/input/input_test.py` injects OS-level keyboard/mouse events through a virtual `/dev/uinput` device (see `Tools/input/input_emulation.py`), e.g.: ```bash -python3 Tools/input_test.py --sequence move:2,look:3 +python3 Tools/input/input_test.py --sequence move:2,look:3 ``` It drives the focused game window exactly like real hardware, which is how the controls were regression-tested on the packaged build. diff --git a/Docs/release-checks.md b/Docs/release-checks.md index 2efbe15..cb433b2 100644 --- a/Docs/release-checks.md +++ b/Docs/release-checks.md @@ -50,7 +50,7 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB ## `0.1.0` — Placement vs references — record (2026-11-08) -Checked against the reference photography (`Art/reference/`) and the Geo measurements (`Geo/verification/*`). Build under test: current `main` content, DemoMap rendered via `Tools/render_screenshots.sh` (2026-11-08, Vulkan SM5, 1920×1080); engine positions probed from the DemoMap (commandlet, 2026-11-08). +Checked against the reference photography (`Art/reference/`) and the Geo measurements (`Geo/verification/*`). Build under test: current `main` content, DemoMap rendered via `Tools/review/render_screenshots.sh` (2026-11-08, Vulkan SM5, 1920×1080); engine positions probed from the DemoMap (commandlet, 2026-11-08). ### Placement and scale (in-engine ↔ reference) @@ -82,7 +82,7 @@ Checked against the reference photography (`Art/reference/`) and the Geo measure - **Crossing ramps are kerb flushes, not graded ramps** — the `0.1.1` Crossing ramp gradient item. - Site identity (the neoclassical hero with motto band) matches the 2009 façade photograph and the 1922 Masonic elevation within blockout fidelity. -Pre-check method: `Tools/judge_screenshot.py` (vision model) flagged the simplified flat surfaces as "unexpected" — those flags are the deliberate deviations above, not placement errors. Final verdict is by human review of the shots in `Screenshots/demo/`. +Pre-check method: `Tools/review/judge_screenshot.py` (vision model) flagged the simplified flat surfaces as "unexpected" — those flags are the deliberate deviations above, not placement errors. Final verdict is by human review of the shots in `Screenshots/demo/`. ## `0.1.0` — Collision and packaging — audit note (2026-11-08) @@ -90,7 +90,7 @@ Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselfte - **Transect (holes/gaps):** 1456 samples over 16 rows — lane centres **plus the kerb/road/sidewalk seam Ys** (−1382, −1053, +718, +1047 and neighbours) and X −20000..−2000. Result **0 holes**. (The old 84-sample check covered only 3 lane centres; the seam rows are what this check adds.) - **Invisible-wall probe:** horizontal traces 30 m long, grounded at 160 cm above the local ground, along the road and both seam rows. Result **0/35 blocking hits inside 21 m** — no invisible walls or misplaced blockers in the street envelope. -- **Facade occlusion probe:** from the road centre, 40 m traces sideways at walking height toward each frontage, required only where the map stages a building (the spans are listed in `AGigLineCharacter` and mirror `Tools/ue_build_demo_map.py`). Result **0/9 directions without a blocking wall** — the player cannot walk through the hero or the frontage. This probe found the hero façade had **no simple collision** at all (complex-only, so traces and capsule sweeps passed through it); it is now `UseComplexAsSimple`. Auto-generated convex hulls were tried first and rejected: on a 95-part, 26 m façade they bulged into the street (the wall probe went 0/35 → 2/35). (First pass used a fixed Z 160 start; the street is at Z ~366, so the probe started inside the earth and reported every row as a dist-0 blocker. Grounding the probe fixed that measurement bug.) +- **Facade occlusion probe:** from the road centre, 40 m traces sideways at walking height toward each frontage, required only where the map stages a building (the spans are listed in `AGigLineCharacter` and mirror `Tools/unreal/ue_build_demo_map.py`). Result **0/9 directions without a blocking wall** — the player cannot walk through the hero or the frontage. This probe found the hero façade had **no simple collision** at all (complex-only, so traces and capsule sweeps passed through it); it is now `UseComplexAsSimple`. Auto-generated convex hulls were tried first and rejected: on a 95-part, 26 m façade they bulged into the street (the wall probe went 0/35 → 2/35). (First pass used a fixed Z 160 start; the street is at Z ~366, so the probe started inside the earth and reported every row as a dist-0 blocker. Grounding the probe fixed that measurement bug.) - **Walk / reset / pause:** 761 cm in 2 s (Z steady 456), reset 0 cm, pause `IsGamePaused()=1`, unpause 0 — all PASS. - Packaging: the packaged build launches, reaches gameplay, and the QA run quits cleanly. The `GigLineShotActor` screenshot rounds are suppressed under `-qaselftest` so they cannot quit the run before the audit phases. - Caveat recorded: ground Z extremes (−12 m / 3000 cm) are trace-start/terrain-edge artifacts of the widened extent, not walkable holes — the walkable corridor is continuous (walk + transect both PASS). diff --git a/Docs/setup.md b/Docs/setup.md index 5b769a6..dcbaca4 100644 --- a/Docs/setup.md +++ b/Docs/setup.md @@ -91,7 +91,7 @@ Two tools talk to paid services and read their keys from the gitignored `.env`: | Variable | Used by | Needed for | | --- | --- | --- | -| `OPENROUTER_API_KEY` | `Tools/judge_screenshot.py`, `Tools/facade_detail_concepts.py` | vision pre-check and AI concept plates | +| `OPENROUTER_API_KEY` | `Tools/review/judge_screenshot.py`, `Tools/gen/facade_detail_concepts.py` | vision pre-check and AI concept plates | | `MAPBOX_ACCESS_TOKEN` | `Geo/tools/mapbox_buildings.py` | the second building-height source | Nothing in the build, the tests or the release path needs either key. Never commit `.env`; `git check-ignore .env` should succeed. diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap index 6676a20..2b4e14f 100644 --- a/Game/GigLine/Content/Hollywood/DemoMap.umap +++ b/Game/GigLine/Content/Hollywood/DemoMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44d2d7eea242e1bad36b0409938820aa3b8631e6dbe044b39b0f835bd713a9aa -size 886541 +oid sha256:974b7ca3e2134bbcfe77b63eca6ece122d885477f938eb1a1b6c6ef695040918 +size 159879 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_00.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_00.uasset index acb079a..10a6bae 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_00.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_00.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ea76d0ba74be79b38b749cf3380f31f29d9f29a561b5f9f4c6059018c9bf0d5 +oid sha256:81b502b85656090db179ce54ea648a525b1d09693b15b2cef840662ae790f970 size 73194 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_01.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_01.uasset index 59f3c22..7708173 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_01.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_01.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5463920682ce3e0acedb2374d761d2ad31102f960b5ea5b4222cdf30384d825 +oid sha256:e129f402185a8a3f25db6e846b56ee39ebc05efe4f2eb8165c0a0e4ce02ccd72 size 73185 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_02.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_02.uasset index bb22288..51eada8 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_02.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_02.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94260f281d51dec4044da961fc2edefdf114c3ded0dc404fe10eeb9be1c0252f +oid sha256:626bf4750f6caf8d5381758c63e2bccd4a9439f02e051e9f329452cc903d7e3c size 73155 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_03.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_03.uasset index d8c70ab..5cbfd56 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_03.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_03.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c87a80f354fd35cd0085d4f0dc12b9fec556e78832dc834a770bb1aff9086a9d +oid sha256:ffdf9a6e607233aa7f1c0d71c8cd67ccb7fa9750ef9ad72b28abfc03eb9a3a3b size 73136 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_04.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_04.uasset index 6b6e434..6df25ce 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_04.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_04.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c149d51ae910e3b4ff309e31c9ec0841f8a26199360b419542cd218d951e00c +oid sha256:17bd9433a6bcf85b69988bcad38f5a2ba928de0731be094871df60eb351e91af size 73124 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_05.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_05.uasset index 84bd780..2d1f612 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_05.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_05.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c81a3dfd251191d59d06f540e9ae5041864ea2b3d7da09ca3ca3b64a0a086d75 +oid sha256:459c8fa3010d92e0fa5374920de73b9ecd52477adcad05aefea42326c676b361 size 73113 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_06.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_06.uasset index e840cf0..29ccf0a 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_06.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_06.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25063190d76d4204bf6b7e25dd2f7b1e0d15952c39b57569a39009712af2dead +oid sha256:bfde72ed0d3ffea7d0510783fdc0e8c12426b5c848ff2cca8cafa8363d867cb9 size 73146 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_07.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_07.uasset index cafccea..6dd78ed 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_07.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_07.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97edaf3298984894b61d81e3aab81272a37c8195dbaa1431a141e5fca3cfba9d +oid sha256:59bba3c02f144e64fcf0a2968a737f95bfb45ff755f2fbae74c579628440e173 size 73088 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_08.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_08.uasset index ef4af2e..6185849 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_08.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_08.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a356bf37313a9a35d01014e4d792791f65c6de9377b737816eff7f727288529e +oid sha256:a4b6ba39f0074b05840fdee0e1d0661141e00f77276797e7f08798ee7ee643ad size 73111 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_09.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_09.uasset index 5e2982b..83528f8 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_09.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_09.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04168fc06162e204b5c5e408377573117c9d8b8cff43be67862839fa2235baf1 +oid sha256:3641ba73ea06926bc6341bb4cf6814b79ae00a264946b2e66821b69775dbe26e size 73100 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_10.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_10.uasset index 7c2d21b..dc8a34b 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_10.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_10.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:682f2a3cd9207793b01204666cecc46e7354b2881cca3337fdca597511117efc +oid sha256:afbab39eb63c03bb05259f77dfff5943472d1f145988f54aa834d0534a1ff294 size 73117 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_11.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_11.uasset index 632f2ab..50d4751 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_11.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_11.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd1c8c41915f22081b81c1b76a39adfe11d60d3e25bc148c5db1779bb77bc370 +oid sha256:d892a8a2c757c2af43962f9af7e5507e932d9cfd360b6a826ad867c239a968db size 73138 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_12.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_12.uasset index 9537e62..2685948 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_12.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_12.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdfa2f1e70e6f103101d4e7765fa81bc58319ba186ec75c28e10df039af48c0e +oid sha256:002feb3aea4a6c15f7009f47f37170c46943f7851d49f37e165906ba081a292c size 73153 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North_13.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North_13.uasset index 7886b08..b9ae385 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North_13.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North_13.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c2b132a1697e8d68670c7de0d52b1bc74af276ead32578ed7ca2ea0ea5687a7 +oid sha256:69b5a4099ce933cfef8d787b42cd87d934e0d5d62c038063275f628f15a1f93d size 73176 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_00.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_00.uasset index 04472be..16653bf 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_00.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_00.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a12cb396f7d27b4207e4acdd6139040b864b64a10a6c6f8884e70780f70e21e +oid sha256:d9150b3f4990ce141d7c84b55ae1d844fbebe23dbb4110afbe37cd31382d1c1c size 73140 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_01.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_01.uasset index c97aae9..1a03b30 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_01.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_01.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eedfd96b688dc2133bbe9ccc228739b2286b15c17bfebbd3984e5068a031b3f8 +oid sha256:b8968ae21946a8bcaec1a2c91f30d813bde4cb99c3970fd89e7b58e67ed2526f size 73209 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_02.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_02.uasset index b83fb3c..074bc3e 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_02.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_02.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15bfef2810f5222d42fc37726fe0dba7c560648384df5ccabf318743bc52da1b +oid sha256:d40da41f64c3e8aa00511f3c1af5fee086b8ab6b62ea7c60224a9ca8623f012e size 73164 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_03.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_03.uasset index 22bbc87..cc9908c 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_03.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_03.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e7b50f14913e63f44875857ff634437c3203a5335b3375e7f0823d85cd12f3f +oid sha256:f37f82a94275e74c07808e034b138a12c8c831dbca1022a7764ab64445298d8f size 73143 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_04.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_04.uasset index 00646b3..a77ba24 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_04.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_04.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4e9246d500864324db1383433baad7c0ec52c422689eb2672cca4463fa83ea4 +oid sha256:aae634238fbb8a6daf7ab3b5313bcd83616babd92533c77480a5c206e713adfc size 73118 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_05.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_05.uasset index 9c1c679..4cdebdb 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_05.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_05.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:739b8d50f1f443299edde4f20526c65e0492e7a1389bd7fdaa43e0aaaf7e5c09 +oid sha256:8277dc63fb4e3057bacf4bf881e5d455010e9c5804227c80f0810f3880976e36 size 73122 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_06.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_06.uasset index 61093e0..fd0d2a1 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_06.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_06.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4becd6a4aad55ef2a1208ae12e52f74746f42395f542cdeedb3210454b6e6ce9 +oid sha256:65f48285716e9fa6b1bcc4804c89f91854f0adbeca1f7e05d8906eb2ed7dc76e size 73169 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_07.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_07.uasset index 169a2e4..783823c 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_07.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_07.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b733eebf5928b093712a5066efb319e36f1c62e8be96a85c0340fdef9076f08 +oid sha256:b0beddcbfa6740cb1ba1fc00297fdc425da2c4da509db1435bc2e74d12eddb3d size 73110 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_08.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_08.uasset index e472a85..857a614 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_08.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_08.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9575ac303871e174cef45c19b7ff448a8a65bed113bb5fdce4f802b1c601cb51 +oid sha256:5d8938b332f7bd988851177d7692c6508b505dd64988fc27ab356c12eb54062d size 73119 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_09.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_09.uasset index 5371444..193e8d8 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_09.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_09.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc72fdf3e822a871ac28a2fbf4c6ad49a87002ae060f8b0bf013dead6cfdf44a +oid sha256:9cee9a0d0b9ee5576372a0821be6c421fc6abf1d8f8cb0656b95e7ce07055150 size 73156 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_10.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_10.uasset index c24a239..c609445 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_10.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_10.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c41639fbc5d97c64c261ed808f947c88e876d88544689a92a38d53c9caee1a56 +oid sha256:4704ac1ae925289769355caad1e0de48ff4d5c70eb8fcfc05cc66a2bf0e7ec7b size 73193 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_11.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_11.uasset index a89e23f..1f3a57a 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_11.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_11.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e9a2b92fadd4fb5b223e25dc9638cdc21215a2eabe37e4eddfe788ecc481fc4 +oid sha256:66ea1a3ae509b897987c8ad1f96825858158186824990afd24394c6d69a3faea size 73176 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_12.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_12.uasset index 1382334..2f284fb 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_12.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_12.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc39a07ea038084b512391ed1bdeedc2ceeee0ec4203053ef706d2f9459b4f52 +oid sha256:6a7df0117d7aed6505f2cf8140a81b3dd49594aad2713afaeac04521aea1cd22 size 73178 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South_13.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South_13.uasset index dfa3fdf..747fdb2 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South_13.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South_13.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ed3e95b9d76e2e941b67d62289e39be45162fee1e7393aacddd5e8b4caed440 +oid sha256:dedb42d0d07b38dddac5aed0def3e7cad083463b67d7ee9133750c67567ceaf1 size 73157 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset index def6f33..d19ea8a 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d4e0a4c7ea1ea14a4456dea0c2c038dceb06f45609de1f81abace13b326e092 +oid sha256:3ea23dc47b149ffa2069c39b8284567d8cb0efb9149028fa728275c005c71698 size 10019 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset index dde3d57..b485e03 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bce0d410f1b8f235dd529b4e42ff0686edd27cc89352f5cdec568a1c3ddbbe6f +oid sha256:4d00026d61673d91e16ae5bc70322e4955e7a9dd4a0628e966c18b6c781ac249 size 10024 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset index 300c86e..fb95f31 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8c10075e2b95b93227581626b352632699e622ba3b9cd40abdd8304c5069bb0 +oid sha256:6ea4f08fc6ca45eca926fd766115b1f34c9981cd180e1f6e0f194170a8634935 size 11419 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset index 155a249..d2eb966 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1957a018886a2431eb640daacc853ab795e51755212ba24bbe6fcdb3130f43cf +oid sha256:78cf6290f2e7753815326f3e97deaacc7bcfb1536acf2809122c650ec0af3a5e size 11414 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset index 17df910..cb586dc 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26d0f2851a5bc547b776482fdfd345b3d50c419f3105308311e2ea7f2a8cad8d +oid sha256:e72bec162a1871ebcbd5faf5c6ecd7dbd87bc8232d11afbae3f75245528b92e2 size 17083 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset index 1569e87..d80ab3e 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d9eb1c8463edd56122fefb1fed79ec41d2983238ca1d7ce0044724bd4f1a02f +oid sha256:057b188ef345fca0652dad41abf4623503dd7f34ed89742719354aa6918873b8 size 17113 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset index 1c339dd..0139a94 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcc87501553c8cb07b886be661949b0dbe1a931999a3b6a1481e15bd5b303a7b +oid sha256:2d41cbcab4017fff95f50518561c635d008f48ea03a9718e9868141442cb15a3 size 10014 diff --git a/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset index b28f2d7..f60674b 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e422c61bd7bda99bdac0dd3ae8d4eb105f9511134638e21ff52288e1406c4add +oid sha256:af104f2d73d95bb55eecb5684c4d574579145c3b026aadc81fe6bdf886611b3e size 10044 diff --git a/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset index 34131db..4a368bf 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eec2e25664740400af8aa6b5ae7c6ea29ade7cac59e4323faa2f5d47fec22c27 +oid sha256:48d3cf184c0660f9092bb369b9bc99d71bac9b85c1ee2638d7291f3e46503c9f size 10029 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset b/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset index e19f5b8..801aca2 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fba358a636ca006329b02339ebc365f36cbf417779c92ec4eced0b7ddd25fd4d +oid sha256:6eaaecca6c86f8575fc91ff4566b6592e91b4f85a95ac66d875a724c6eb47a9c size 11389 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_00.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_00.uasset index c52ae3c..962460c 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_00.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_00.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9fddec2baa042a747aa4bc42b6f6cae28be24bb2f5c84bf581efb281822ee9b +oid sha256:232c85dd7acf5be1bf037d498c291e07c5d572d589245dcc8789f8ddbae4d75d size 80253 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_01.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_01.uasset index e280b4a..2efbac5 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_01.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_01.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:123fa1a51622a835a61a2bb6c92311f0a17dc35afb6696f4546b8d821e1d22d0 +oid sha256:8eb759c537cfff6a89f16a0b21811ec03d3a2d9c673ea806077739f0e57e5e9c size 80641 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_02.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_02.uasset index 81acd4e..e2663e6 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_02.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_02.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:595d541dcb590f218c9c3d79b82125236cf6fede88e6e2d857b93e1afe874282 +oid sha256:43f3cd0c4951de05ed92d2b980da19c748453b782b7cfd0739f7a1272e8f6a28 size 80982 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_03.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_03.uasset index fac5f3b..cb767ae 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_03.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_03.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:727e3c4282ffdffdbf143e80b1366624bb941382c490c91855851f00be2b4844 +oid sha256:deda92d889f19373a3342dcb937a00ea343e1981e9beca1625ac22484d28b344 size 80950 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_04.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_04.uasset index 854c3dd..159398f 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_04.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_04.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a352c774aded9c7dbc91ddc8117e6c158ca3386bb8e3b66fde74a06edb58a984 +oid sha256:11413ac1715b477b4e215c17210e1b0de15694d8392c699c7ceba37e99bf5eeb size 80997 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_05.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_05.uasset index c41c2ff..09cc282 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_05.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_05.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a971c1ea6abbe4c0090704599729bd76d1cdc4d4f7109db15ffbd94d0bc45b3b +oid sha256:59a6942b9f59922e3fd15af05642a55dc4a3b1bbbc79ac871ff95ef62bc8135e size 80903 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_06.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_06.uasset index 4ba21d4..333b5fe 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_06.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_06.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b157c61e80a15ea8beb5f4b4059a61332d475fa8202c30e26e8d05370a7293a4 +oid sha256:780a20ac67040095f00afc9b030342a7536c78f26b476acb2acd7a8612447be2 size 80961 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_07.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_07.uasset index b015a26..8447a9e 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_07.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_07.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b6aa92f35a3c9189bde19a7469e0ad7e99a9fa0d8310eb43e8dd5d17fbff19b +oid sha256:c791a921283866a6b7f05a047ea029dcc4122052c6c1bde656a1daa8ce9a8060 size 81003 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_08.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_08.uasset index 0b6769d..f4ced8c 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_08.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_08.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:062213da44007016a0ea5e9b913743be718d99387dd191d364cea27a04be9dfa +oid sha256:6684de78f657e9ce34ad399c11134cce5c5dd75e6cb546662cc2261ca302ecbd size 80528 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_09.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_09.uasset index 1885043..416459d 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_09.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_09.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26c7b3786f4c06528c80e00698f9a4177a2bee14e2d9f0133d2117d2c5c1d2ad +oid sha256:fd15e841ab65ea59a6b5bdfa99c3ba9ecf0d0fbc8cf7ec1e784fafb9f73454b1 size 81123 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_10.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_10.uasset index 2a17648..8813303 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_10.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_10.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83486e37216672c053908f9de1962a69eda9c6bda254aec78c90cfba87b7a8ec +oid sha256:01034ac547f1bd706b5fbb10f67b3e6bd6b2bcc6012e08618895f1534d25eec8 size 80895 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_11.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_11.uasset index 0ba68e1..9ede73c 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_11.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_11.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef31809b6fd4fc84ce87189159a27a45ac7f16306837d81fde0cc27aa9064e37 +oid sha256:55baecee28f2b80b59f98660a721860a2877528b1f3c3e4106dbbd6d80156d0e size 80945 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_12.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_12.uasset index 697e3b0..9daba45 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_12.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_12.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:800f5215be29710532a16cd7c8b42c95337458997b154dd2ad84df2bbe7015f0 +oid sha256:a94f98a8959f1d3ffed76b6a1696993357aeae720aa0f22871a849ae969c4e20 size 80860 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_13.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_13.uasset index 585c05e..2c4136c 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_13.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North_13.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83e396faaf332a5981adf37256423f0db6bd5c4573d4ffa03e21c598aa026054 +oid sha256:8b78c32143e7245ff051aa9ee07893a20929b948df6da662f4966f3ef82d0260 size 80488 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_00.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_00.uasset index 005d85d..2e6127c 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_00.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_00.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8a4436fc7ed298b0fd858fcb54d67a021a92967a921c4823d45bf1348905ab5 +oid sha256:4731464a344a3e81b098c90e11948529aa3428eff740e9d708925a9546557a80 size 78792 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_01.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_01.uasset index d3691cf..cf66025 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_01.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_01.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00d03153f1e6b4f049bd00d6ac8f762321590f4ad16fd44288264a47bf5868bc +oid sha256:7876b334c6d6b21d571d2c362514558422158f9d26fee3b595994c9b26b07afc size 79045 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_02.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_02.uasset index e20745a..8269820 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_02.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_02.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:871e19829a6e36ac43d6d6343a57ea6d9f31668c1f4bcaa9c8890a80becc9d21 +oid sha256:f15e5e6ce0c044f2d1a60e6b6a98cbec082c16d297d829bd26efab0019681c27 size 79103 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_03.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_03.uasset index 41549ed..50ad133 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_03.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_03.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bcdf01ebb0a0f641108b8b46a8f9f826ca5b7a1de223f4c74bb368b26fdf1fc +oid sha256:951bbd4b46c117916f2a9552542fe2428bef97da5eb070460a478982194f1c9b size 79059 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_04.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_04.uasset index 7ab4939..5c8ef8f 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_04.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_04.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:463f43a70d399fdf62457edcc4a057981c25422b6270958c7909e45260b213dc +oid sha256:cb75f0297cb638394f066cbf25b96de5539fc525920c290f62f7964bc5859dd2 size 79007 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_05.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_05.uasset index 3a79981..87895a2 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_05.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_05.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a2d34e2796ec4341d8b0273c3f7dc25d827ac70045565c6753c924ce6ee43cf +oid sha256:9476c130da98d68c4b2ef890aed7271a3dcaa919f591a2990a0976fea18e2f8e size 79084 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_06.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_06.uasset index 9a0f1b9..157ed45 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_06.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_06.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c79ccb3e0777db2e7cb591ca617492bbe92ff4863b6a51c262bb7b56758c2712 +oid sha256:39ba83fc5ce6c4cfa178665a96ec3dd02a1a48f185115c74b3a77fa80fa7147a size 79119 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_07.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_07.uasset index 010f24d..778fb42 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_07.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_07.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ada61754b5c689d28e2e3e471bf1db35fee23d98407744b64084e00f65ce6b6 +oid sha256:dea4c4813ebe030a8d9b576ecb85c7b40b008ce34bb1c8a6c10d6b33f7557374 size 79166 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_08.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_08.uasset index 1c73f0b..ab724b1 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_08.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_08.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01b945f60910249e763dd3a2039cd346ee7c6d37695d3d8a770ac66c5bdbed83 +oid sha256:2966566df7c679b351079baabdd550f20b50d40c1ee1061ecb2949b246d80750 size 79316 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_09.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_09.uasset index c881f69..1f8fbd5 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_09.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_09.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee7669bd45e2ae64c70a183f90722c0fafeaed8df3cb43c183a3b3983a844196 +oid sha256:e51f5f7e9a09ee0b5c9d5f8925868391c79ba45faac7db81b4687b810818acbf size 79200 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_10.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_10.uasset index e2d5464..2a1b491 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_10.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_10.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf81a7ed6edfbbec7280764f2bd65f770df47489101ae7e645c300ccaf819dd3 +oid sha256:a67c8f3ed9fefda750bc211040cf34ce21c39f765a56d0cb4932832fbac48d64 size 79252 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_11.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_11.uasset index d1cd6bc..676179e 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_11.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_11.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afe1745b8b595c2129097cfd2fa9d1b65875a96b9688122d6aa1360c7d1497cb +oid sha256:80296be16a2d622db5c05b6f1ab229ade5d905999ee8498605518c36c1a3660a size 79277 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_12.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_12.uasset index c5cadf2..7c9f268 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_12.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_12.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:740a358977ba0bc42e4592b0dd8c2606089400f38579fa435039e97c70264412 +oid sha256:b86569d52043841b0a98613bf00ff100671c1ed6ba704769072dac32da10c99c size 79139 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_13.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_13.uasset index b0932f4..18de183 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_13.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South_13.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce8e57655e79ce04f868d300eff0b50cee5027830749a21d992bc2f3b40f0fcd +oid sha256:a3c68c0c457a731d1182b46b71b917b0c894662330e61223087b912109f92803 size 78786 diff --git a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset index d3b5e35..34d7b28 100644 --- a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:208aee4ad5aac67e220d352c7bf4165febbc0aae2dcdf21c750acfeed4e5972d +oid sha256:9fb21a2bcbaa9384514224a45b2d7247c9b9472dc6efa0bb99988eddb6d403be size 355236 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset index 56bb0d5..1493e28 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd360b93d4ef5b81184a5a03c8f21ae377aa4264583c37dac00c83c513a6e656 +oid sha256:7064f90afdefeb0852b81d871f097bf2a4fcc7ea314cbd311a8ea68334a63822 size 70879 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset index 3849222..a92d521 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b06905b59fa094453700869a10a413d0a8d1192340ef1776b32c275ffe30b308 +oid sha256:1152118614d95e9529423e2f45027f2df5f6cb90da5f4b67fa3adc978999dc81 size 29011 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset index 40ab95f..e9fda95 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97ba1286b5aaa67ab2da1a20e2ac3f1163901eb34a42242efc1242def942559f +oid sha256:7fa3ea162968333e39f20c265cac4c955c6ee2e336975fe23fad3df4595e0d27 size 58568 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset index 23f8bff..10b77d0 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a16debfa288545288b0727d9f6b04372184d8cfea0c829dfe816eef5814849ba +oid sha256:71236a943b3ab6305afb936cd8c96f934679dbdd91bcc502d313ae87f0d818a7 size 17517 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset index 06ad2dd..7e4245d 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33466b6cc8717db7edc02eb623fe6a9e3c0fc536ba2bff7661e7552cace2f837 +oid sha256:5da33fc8e79cfb7abb1374cc330e73959d23477569d67f56cc7bb2ddece82692 size 40610 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset index 932b8d6..8eb46c1 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11db9471960053108679e30cb73b9f829ab63c4e1fe99e11699ba41cde7f0f80 +oid sha256:650fdd15845125e51f520596817dbd698035f4fd78085a989bc4dd644dd0f9e8 size 18770 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset index 31f59cb..c55950d 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:363ed1e1c31b71f69a3e9f7befc2c679d2307d4e017274321a875719c4576360 +oid sha256:ac83885d1a2fea37f657d398474ef9bbf8f41348b0b6d44ccc40a5f9a1392bea size 62746 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset index 452f3cd..7f7da71 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5470a9f43e816ab8022d62a209ab874d71c4aa28b80e71767c624f299779dda +oid sha256:169589881b043da1f14cf49a3f711180b1e54c1cfd028640382c121ecdf954b9 size 17541 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap index 81981e1..056e8a4 100644 --- a/Game/GigLine/Content/Test/ReviewMap.umap +++ b/Game/GigLine/Content/Test/ReviewMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fad806f3f77e73af8134565887962e7548a74aafde64b59fff2123f6dce23f72 +oid sha256:c4c6786ffb3413d907dbfe44a35316525ff299ff42ef039fe99bf9940ae7d227 size 38367 diff --git a/Game/GigLine/Content/Test/_ProbeLevel.umap b/Game/GigLine/Content/Test/_ProbeLevel.umap new file mode 100644 index 0000000..758fe60 --- /dev/null +++ b/Game/GigLine/Content/Test/_ProbeLevel.umap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa9f965703eb8cdb9a2cdd843c5fd75b7be67d73307f7e0c543b3697f7fda1a4 +size 6368 diff --git a/MEMORY.md b/MEMORY.md index 9a95414..77f1063 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -65,10 +65,10 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-07 — Reference photography - **Licensing is not a gate for local development.** Reference images live in the gitignored `Art/reference/` and are never committed. What each set establishes is written into `Docs/reference-notes-hollywood.md`. -- **Commons text search runs dry; categories do not.** `Tools/commons_fetch.py --category "Category:Hollywood Masonic Temple"` surfaced 15 files a search never showed, including 4–5k px façades. +- **Commons text search runs dry; categories do not.** `Tools/gen/commons_fetch.py --category "Category:Hollywood Masonic Temple"` surfaced 15 files a search never showed, including 4–5k px façades. - **`upload.wikimedia.org` throttles hard.** Batches of six or fewer with `--delay 3`; a throttled file is skipped and recorded rather than killing the run (HTTP 429). - **Unqualified queries return junk.** `Hollywood Boulevard shops` returned a Münster street, `Hollywood Masonic Temple entrance` a Seattle card index. Check the hit list before accepting a set. -- **OpenRouter web search is expensive.** One `gpt-5.2` + `openrouter:web_search` call ran 17 searches and cost **$0.38**; two of its eight URLs were Commons files already held. Use it for a known gap, not a sweep. `Tools/page_image_fetch.py` turns results into files by reading `og:image`. +- **OpenRouter web search is expensive.** One `gpt-5.2` + `openrouter:web_search` call ran 17 searches and cost **$0.38**; two of its eight URLs were Commons files already held. Use it for a known gap, not a sweep. `Tools/gen/page_image_fetch.py` turns results into files by reading `og:image`. - **The inherited "eight stone steps, 12 m wide" is wrong.** Photographs show three portals, each with its own short flight. Do not copy that number into geometry. ## 2026-11-06 — Blender and Blosm, headless @@ -98,12 +98,12 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-07 — Unreal pipeline findings - **UE 5.8 game targets must use `BuildSettingsVersion.V7`** to share the installed editor's build environment; V5 is rejected with "modifies the values of properties … not allowed". -- **`UnrealEditor-Cmd -run=pythonscript` swallows Python `print()`** with `-stdout` in 5.8 — write results to a file instead (see `Tools/ue_scale_test_import.py`). +- **`UnrealEditor-Cmd -run=pythonscript` swallows Python `print()`** with `-stdout` in 5.8 — write results to a file instead (see `Tools/unreal/ue_scale_test_import.py`). - **`unreal.Box` has `min`/`max`, not `origin`/`get_extent()`** in 5.8 Python; compute centre/size from the corners. - **FBX Blender→UE axis mapping is east→+X, north→−Y, up→+Z** with preset `ue_metric_v1`; the Y mirror is inherent (RH→LH). Compass math in game code: north = −Y. Verified with the three-cube scale test. - Headless asset import works fine with `-nullrhi`; `unreal.AssetImportTask` + `FbxImportUI` is the pattern (Interchange pipeline under the hood). - **Flatpak Blender headless needs `XDG_CONFIG_HOME=$HOME/.var/app/org.blender.Blender/config`** for extensions: `bpy.utils.user_resource('EXTENSIONS')` otherwise resolves to `~/.config/blender/5.2/extensions` (empty) and `bl_ext.blender_org.mpfb` is undiscoverable. Blosm unaffected (scripts/addons via `BLENDER_USER_SCRIPTS`). Found via `Tools/addon_compat_probe.py`; documented in `Art/blender/README.md`. -- **UE 5.8 Interchange skips material/texture assets for skeletal FBX imports** (static imports do get materials). Finish the chain in Python: import texture, build Material via `MaterialEditingLibrary`, assign with `sm.set_editor_property('materials', [SkeletalMaterial])` — the struct field is `material_interface`, not `material`, and `SkeletalMesh.set_material()` does not exist. See `Tools/ue_character_import.py`. +- **UE 5.8 Interchange skips material/texture assets for skeletal FBX imports** (static imports do get materials). Finish the chain in Python: import texture, build Material via `MaterialEditingLibrary`, assign with `sm.set_editor_property('materials', [SkeletalMaterial])` — the struct field is `material_interface`, not `material`, and `SkeletalMesh.set_material()` does not exist. See `Tools/unreal/ue_character_import.py`. - **Blender 5.2 FBX exporter removed `armature_nla`** (passes for 4.x recipes fail). Also: a baked image is only referenced by the FBX exporter when `image.filepath` (not just `filepath_raw`) is set and the exporter can map the node — MPFB's v2 skin node-group tree can't be mapped, so a flat export material is required. ## 2026-11-07 — Collaboration and scope @@ -122,7 +122,7 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. - **`unreal.InputCoreTypes` does not exist** in the 5.8 Python bindings, and `unreal.Key` takes no constructor argument. Build a key with `k = unreal.Key(); k.import_text("SpaceBar")` — the export text is the key name (`MouseX`, `Gamepad_Left2D`). - **Factories are `unreal.InputAction_Factory` and `unreal.InputMappingContext_Factory`**, not `InputActionFactory`. `create_asset(..., factory=None)` also works. -- **Do not delete an asset and recreate it in the same commandlet run.** `create_asset` returns `None` for a path deleted earlier in that process. Load the existing asset and overwrite it instead (`Tools/ue_create_input_assets.py: fresh()`). +- **Do not delete an asset and recreate it in the same commandlet run.** `create_asset` returns `None` for a path deleted earlier in that process. Load the existing asset and overwrite it instead (`Tools/unreal/ue_create_input_assets.py: fresh()`). - **Modifiers on `EnhancedActionKeyMapping` do not survive saving.** Assigning `InputModifierNegate` to the mapping's `modifiers` reads back correctly in memory, even after a `gc.collect()`, but the saved asset reads `mods=[NoneType]`. Keep-alive references, explicit flag setting, and in-place saves all failed to change it. Workaround: bind one action per direction (W and S as separate actions) so no modifier is needed. - **A read-only packed `.umap` silently blocks `save_map`.** `EditorLoadingAndSavingUtils.save_map` returned `False` with no error until the file's mode was fixed (`chmod u+w`). Symptom: a rebuilt review map keeps three cameras after a fourth is added. - `-run=pythonscript` still swallows `print()`, so write results to a file; `/tmp` is visible to the commandlets even though it is not shared between agent tool calls. @@ -153,7 +153,7 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-07 — Blender rebuilds that silently did nothing -- **A read-only `.blend` makes Blender skip the save without failing the run.** `Tools/hero_facade_build.py` printed its part list and "bounds" as normal, then went straight to the export: the `.blend` and the report stayed at their old timestamps, and the Unreal re-import brought back the old mesh. Symptom to watch for: the imported bounds do not change after a rebuild. Fix: `chmod u+w` the `.blend` first, then confirm the timestamp moved. +- **A read-only `.blend` makes Blender skip the save without failing the run.** `Tools/blender/hero_facade_build.py` printed its part list and "bounds" as normal, then went straight to the export: the `.blend` and the report stayed at their old timestamps, and the Unreal re-import brought back the old mesh. Symptom to watch for: the imported bounds do not change after a rebuild. Fix: `chmod u+w` the `.blend` first, then confirm the timestamp moved. - **Model bottom must land on Z 0.** The hero portal jambs were sized `PORTAL_H + 0.3` and centred at `PORTAL_H / 2`, so they hung 0.15 m below the pavement and the whole facade imported with `min z = -15 cm`. Always check the asset's `bounds_m.min.z` in the build report. - **Rebuilding a facade is not enough to make a detail shot work**: doors modelled as thin panels on a solid plinth read as shading, not openings. Cutting the plinth into piers with a lintel band is what makes them legible from 8 m. @@ -180,8 +180,8 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-07 — screenshot judge tool -- `Tools/judge_screenshot.py` — runs an OpenRouter vision model over a screenshot vs an expected-scene description. Reads `OPENROUTER_API_KEY` from the repo `.env` (ignored). Base64 data-URI image under `image_url.url`; OpenAI-compatible body → `POST /api/v1/chat/completions`. `openai/gpt-4o-mini` returns a `{verdict, confidence, reason, missing}` JSON. It flags genuinely missing content (e.g. `main`'s pre-warm-skin character shot judged "no"); it is a pre-check, not the human verdict. -- Test one: `Tools/judge_screenshot.py --image Screenshots/review-01-character.png --expect "" --verbose`. Bulk: `--shots` (parses the README rows). +- `Tools/review/judge_screenshot.py` — runs an OpenRouter vision model over a screenshot vs an expected-scene description. Reads `OPENROUTER_API_KEY` from the repo `.env` (ignored). Base64 data-URI image under `image_url.url`; OpenAI-compatible body → `POST /api/v1/chat/completions`. `openai/gpt-4o-mini` returns a `{verdict, confidence, reason, missing}` JSON. It flags genuinely missing content (e.g. `main`'s pre-warm-skin character shot judged "no"); it is a pre-check, not the human verdict. +- Test one: `Tools/review/judge_screenshot.py --image Screenshots/review-01-character.png --expect "" --verbose`. Bulk: `--shots` (parses the README rows). ## 2026-11-07 — Blender skill packs: discovery depth @@ -202,12 +202,12 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-07 — ReviewMap corruption: deprecated level calls + save-order bug - **`EditorLevelLibrary.load_level`/`new_level` are deprecated no-ops in `-nullrhi` commandlets** (they return without switching worlds, so staged actors land in whatever world is current and get saved over the wrong map). Use the `LevelEditorSubsystem`: `unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).load_level(path)` actually switches. Symptom was ReviewMap containing Demo actors + `ShotSequencer`. -- **Map-save ordering bug in `Tools/ue_build_review_map.py`**: the script saved ReviewMap _after_ the DemoMap section, but the switch back to ReviewMap fails silently because the map only exists in memory at that point — so the final `save_map` wrote the demo world over ReviewMap. Fix: save ReviewMap first, then stage the demo cameras, and never switch back. Both maps verified by probe (ReviewMap lists Review actors; DemoMap lists Demo actors). +- **Map-save ordering bug in `Tools/unreal/ue_build_review_map.py`**: the script saved ReviewMap _after_ the DemoMap section, but the switch back to ReviewMap fails silently because the map only exists in memory at that point — so the final `save_map` wrote the demo world over ReviewMap. Fix: save ReviewMap first, then stage the demo cameras, and never switch back. Both maps verified by probe (ReviewMap lists Review actors; DemoMap lists Demo actors). - Rebuilt ReviewMap after the fixes: character, both cubes (A grounded, B floating at 2 m), pawn, NPC prefab, hero facade all present; user re-approved the shots. README cube naming corrected (README had A/B swapped vs the script). ## 2026-11-07 — Materials and lighting in the demo map -- **Blender flat diffuse colours do not survive FBX -> Unreal.** Every site mesh (road, sidewalks, kerbs, crossing paint) imported with a material asset that had a default _white_ base colour, and the terrain imported with `WorldGridMaterial` (UE's bright grey grid). That — not the sun — is what made the demo look blown out and washed white. Fix: create the colours in UE (`M_Asphalt`, `M_Concrete`, `M_Ground`, `M_RoadPaint` via `Constant3Vector`) and assign them per actor in `Tools/ue_build_demo_map.py`. +- **Blender flat diffuse colours do not survive FBX -> Unreal.** Every site mesh (road, sidewalks, kerbs, crossing paint) imported with a material asset that had a default _white_ base colour, and the terrain imported with `WorldGridMaterial` (UE's bright grey grid). That — not the sun — is what made the demo look blown out and washed white. Fix: create the colours in UE (`M_Asphalt`, `M_Concrete`, `M_Ground`, `M_RoadPaint` via `Constant3Vector`) and assign them per actor in `Tools/unreal/ue_build_demo_map.py`. - **Auto-exposure makes review shots non-comparable**: the same road rendered washed-out in one framing and near-black in another because each camera saw a different amount of sky. Fix: an unbound `PostProcessVolume` with `AEM_HISTOGRAM` and `auto_exposure_min_brightness == auto_exposure_max_brightness` (fixed exposure). Do not use `AEM_MANUAL` + `auto_exposure_bias` — a bias of 11 blew the whole frame out. - **`DirectionalLightComponent.light_color` takes a `Color` struct, not `LinearColor`** (`unreal.Color(r=255,g=245,b=230,a=255)`); passing `LinearColor` raises a nativize error. - **One band per object loses geometry downstream.** `crossing_band()` created `Crossing_Markings` each call, so Blender renamed the second and third to `.001`/`.002`; `site_assembly.py` appends by exact name, so only one crossing ever reached the game. Build all bands into a single mesh instead of one object per band. @@ -317,13 +317,13 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-09 — hero colonnade proportion fixed (47.6 % → 53.1 %) - The hero's colonnade (columns 2.00–7.95 + capitals 7.95–8.90) was 6.90 m of the 14.50 m elevation = **47.6 %**, below the photographed 50–60 %, because the entablature (3.40 m) and parapet (2.20 m) used 5.60 m of the height. -- Thinned ENTABLATURE_H 3.40 → 3.00 and PARAPET_H 2.20 → 1.80 in `Tools/hero_facade_build.py`; the colonnade grows to **7.70 m = 53.1 %** while the envelope, motto band and medallions keep their relationships (all positioned relative to those two constants). +- Thinned ENTABLATURE_H 3.40 → 3.00 and PARAPET_H 2.20 → 1.80 in `Tools/blender/hero_facade_build.py`; the colonnade grows to **7.70 m = 53.1 %** while the envelope, motto band and medallions keep their relationships (all positioned relative to those two constants). - **Gotcha (cost a bad build):** `hero_facade_build.py` takes `--out` for the **FBX** and `--save-blend` for the `.blend`. Passing `--out .blend` writes FBX bytes over the blend and Blender then reports "File format is not supported". Correct invocation: `--out /tmp/gigline/hero-facade.fbx --save-blend Art/...blend --report Art/...json`. - Rebuilt hero → re-imported (`SM_HeroFacade`, bounds 2604 × 1257 × 1450 cm) → DemoMap rebuilt → screenshots re-rendered. Note `ue_hero_facade_import.py` saves the map, so `chmod u+w` the `.umap` first or the import silently does not persist. ## 2026-11-09 — AI detail plates from reference photos; floating windows fixed -- `Tools/facade_detail_concepts.py` generates architectural detail plates from one local reference photograph via OpenRouter image models (default `google/gemini-2.5-flash-image`; the sibling project uses the same route in `tools/generate_textures.py` — chat/completions with a text + image_url content part, image returned as `message.images[].image_url.url`). Eight studies, `--list` free, `--all` generates, `-vN` never overwrites, `.refgen.json` sidecar per image, all output under gitignored `Art/reference/facade-details/`. +- `Tools/gen/facade_detail_concepts.py` generates architectural detail plates from one local reference photograph via OpenRouter image models (default `google/gemini-2.5-flash-image`; the sibling project uses the same route in `tools/generate_textures.py` — chat/completions with a text + image_url content part, image returned as `message.images[].image_url.url`). Eight studies, `--list` free, `--all` generates, `-vN` never overwrites, `.refgen.json` sidecar per image, all output under gitignored `Art/reference/facade-details/`. - **Prompt gotcha:** the model refuses when the prompt describes features the reference photograph does not show ("the image you sent appears to be a decorative architectural detail, not an entrance with steps"). Prompts must describe the _reference subject_ and only restage light/people/surface. - **Output is real:** 1184×864 to 1344×768 PNGs, ~2 MB each. - **Measured defect found and fixed:** hero window panes sat at Y −1.98 while the mass front is Y −0.50 and the columns reach −1.02 — i.e. glazing floating 1.44 m in front of the wall and ahead of the colonnade. Now `WINDOW_Y = −0.56` (6 cm proud of the wall, behind the columns). Visible as dark openings in `review-hero-facade-front`. @@ -339,7 +339,7 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-09 — one FBX export preset instead of six copies - The `bpy.ops.export_scene.fbx(...)` argument block was duplicated in six scripts (hero, block frontage, north buildings, site, scale test, MPFB character), and all six carried `mesh_smooth_type="OFF"` — hence the "No smoothing group information was found" warning on every import. -- Now `Tools/fbx_export_preset.py` holds `ue_metric_v1` and `ue_character_v1`: `preset_kwargs()` (bpy-free, unit-tested) plus `export_static()` / `export_character()` wrappers that import `bpy` lazily. Every caller imports it and passes `selection=True/False`; the seven duplicated blocks are gone. +- Now `Tools/blender/fbx_export_preset.py` holds `ue_metric_v1` and `ue_character_v1`: `preset_kwargs()` (bpy-free, unit-tested) plus `export_static()` / `export_character()` wrappers that import `bpy` lazily. Every caller imports it and passes `selection=True/False`; the seven duplicated blocks are gone. - Callers in `Tools/` resolve the module with `sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))`; the Blender-side `Art/blender/site/export_site_fbx.py` computes the repo root (`..`, `..`, `..`) and joins `Tools`. Getting that wrong silently inserts `Tools/Tools` — check the import path when adding a caller. - Verified end to end: scale test, hero, frontage (6/6), site (7/7) and north buildings all re-exported through the preset, re-imported with **0 smoothing warnings**, DemoMap rebuilt and `-qaselftest` all PASS (walk, transect 0 holes, wall probe 0/35, facade probe 0/9). - `mpfb_character_export.py` was not re-run: it needs the MPFB add-on on `BLENDER_USER_SCRIPTS` ("Add-on not loaded: bl_ext.blender_org.mpfb"), which is pre-existing and unrelated; the character is not in `0.1.x` gameplay. @@ -357,7 +357,7 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-09 — 0.1.3 packaging/setup fixes -- **`Tools/ue_common.py`**: `make_writable()` clears the read-only bit an LFS `.umap` checks out with (otherwise `save_map()` returns False with no error and the staged actors vanish), and `require_saved()` makes that failure visible. `ue_hero_facade_import.py` and `ue_import_north_buildings.py` never did this — which is why importing the hero appeared to succeed while saving nothing. Verified by forcing `chmod u-w` on DemoMap first: the run logged `map writable: chmod u+w … (was 0o444)`, then `save_map: True`, exit 0. +- **`Tools/unreal/ue_common.py`**: `make_writable()` clears the read-only bit an LFS `.umap` checks out with (otherwise `save_map()` returns False with no error and the staged actors vanish), and `require_saved()` makes that failure visible. `ue_hero_facade_import.py` and `ue_import_north_buildings.py` never did this — which is why importing the hero appeared to succeed while saving nothing. Verified by forcing `chmod u-w` on DemoMap first: the run logged `map writable: chmod u+w … (was 0o444)`, then `save_map: True`, exit 0. - Gotcha while adding it: `ue_hero_facade_import.py` had **no** `import os`/`sys` (only `unreal`), so the sys.path line raised `NameError: name 'sys' is not defined` inside the commandlet — an exit code of 255 with no obvious cause. - Docs: `build.md` gained the current pipeline table (7 site FBXs, frontage, north, hero, map), the `Builds/GigLineQA` archive, the packaged-log path, `-qaselftest` usage and a **traps** table; `setup.md` marks the fresh-checkout build verified and documents the two optional API keys; architecture 07 + combined doc corrected the build-output path. @@ -371,16 +371,16 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-09 — facade texture maps (0.1.4) -- `Tools/generate_facade_textures.py` writes deterministic 512² base-colour and roughness maps (ashlar stone, stucco, asphalt, concrete) with a pure-zlib PNG encoder and hash-based value noise — no numpy/Pillow, ~14 s for all eight, and byte-identical on a re-run (checked by md5sum). +- `Tools/gen/generate_facade_textures.py` writes deterministic 512² base-colour and roughness maps (ashlar stone, stucco, asphalt, concrete) with a pure-zlib PNG encoder and hash-based value noise — no numpy/Pillow, ~14 s for all eight, and byte-identical on a re-run (checked by md5sum). - `ue_build_demo_map.py` imports them to `/Game/Hollywood/Textures` and samples them **world-aligned**: WorldPosition (cm) → ×(1/tile_cm) → ComponentMask(R,G) → TextureSample UVs. `tile_cm` per material: stone 440, stucco 260, asphalt 900, concrete 250. - Gotchas: `MaterialExpressionTextureSample`'s UV input pin is **"UVs"**, and the output pin for colour is **"RGB"** / roughness **"R"**. `ComponentMask` pins are R/G/B/A booleans. A textured material skips the graph coursing on purpose — overlaying both would double the course pattern. - The script needs `ROOT` in `ue_build_demo_map.py` and `sys` imported; forgetting either fails with `NameError` and an exit code of 0 from the commandlet (always read the result file under /tmp/gigline). ## 2026-11-09 — façade module kit (0.1.4) -- `Tools/facade_kit_build.py` builds the kit at the measured **4.96 m bay** and **3.10 m storey**: pilaster, sill, lintel, window bay, shopfront bay, door bay, cornice, parapet + coping = 16 meshes, exported per-object with `fbx_export_preset` plus a JSON part report (dims + evidence per piece). -- `Tools/ue_import_facade_kit.py` → `/Game/Hollywood/Kit/*`, complex-as-simple collision (a convex hull would fence off the shopfront glazing). -- `Tools/ue_apply_facade_kit.py` lays the kit out from each building's own bbox: `bays = round(width / 496 cm)`, storeys from height minus the 310 cm ground floor. **638 pieces** over six frontage buildings + both north buildings. +- `Tools/blender/facade_kit_build.py` builds the kit at the measured **4.96 m bay** and **3.10 m storey**: pilaster, sill, lintel, window bay, shopfront bay, door bay, cornice, parapet + coping = 16 meshes, exported per-object with `fbx_export_preset` plus a JSON part report (dims + evidence per piece). +- `Tools/unreal/ue_import_facade_kit.py` → `/Game/Hollywood/Kit/*`, complex-as-simple collision (a convex hull would fence off the shopfront glazing). +- `Tools/unreal/ue_apply_facade_kit.py` lays the kit out from each building's own bbox: `bays = round(width / 496 cm)`, storeys from height minus the 310 cm ground floor. **638 pieces** over six frontage buildings + both north buildings. - **Bug worth remembering:** the first window piece was modelled entirely at +Y (into the wall), so every window sat _inside_ the solid blockout wall and was invisible in every render while the placement maths looked right. The kit now projects the surround 3 cm proud with the glazing 6 cm behind its face — a solid-wall blockout cannot carry a real recess, so the piece has to project. - Blender gotcha: clearing the scene _including materials_ invalidates handles already taken ("StructRNA of type Material has been removed"); clear objects and orphan meshes only, and create the material after the clear. - The kit meshes are spawned at the world origin with their position baked into the mesh (like the frontage exports), so placement uses the bbox directly and must not add the actor location. @@ -401,7 +401,7 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-09 — visual comparison loop; screenshots are demo-only -- `Tools/visual_loop.py` closes the reference → render → feedback loop. `--verify` gates captures against `Tools/visual_targets.json` (blank, tone-less, wrong-size fail); `--compare` measures luma, RMS contrast, edge density, palette distance and a 3x3 luma grid against the reference and prints a 0–1 score; `--iteration N` writes `Art/reference/iterations/N/{metrics.json,feedback.md}`. The feedback names the skill to consult, so the next modelling pass starts from a measurement rather than an opinion. +- `Tools/review/visual_loop.py` closes the reference → render → feedback loop. `--verify` gates captures against `Tools/review/visual_targets.json` (blank, tone-less, wrong-size fail); `--compare` measures luma, RMS contrast, edge density, palette distance and a 3x3 luma grid against the reference and prints a 0–1 score; `--iteration N` writes `Art/reference/iterations/N/{metrics.json,feedback.md}`. The feedback names the skill to consult, so the next modelling pass starts from a measurement rather than an opinion. - **Iterations are gitignored** (`/Art/reference/iterations/`); only confirmed shots are promoted into `Screenshots/`. A test asserts the ignore rule and that every target is a demo-map shot. - `Screenshots/` now holds **demo-map captures only** — the review staging map is no longer rendered for review. `render_screenshots.sh --verify` renders the demo map, collects the shots and runs the verification gate in one step. - **PIL gotchas:** `FIND_EDGES` leaves its own border ring at 128 (236/3600 px on a 60x60 frame = 6.5% "detail" on a perfectly flat image), so crop **after** filtering — cropping before does not help, the new border gets the same treatment. Palette shares must divide by the full-image area, not the cropped one. @@ -409,7 +409,7 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-09 — theatre marquee + blade sign -- `Tools/theatre_trim_build.py` builds the El Capitan Theatre's signature features in **world coordinates**: marquee canopy 12.0 × 2.60 m at a 4.60 m soffit, illuminated fascia, recessed soffit, vertical blade sign 1.30 × 5.20 m with frame and bracket. `Tools/ue_stage_theatre_trim.py` imports to `/Game/Hollywood/Theatre` and stages one actor per piece (single spawn at the origin — no placement arithmetic). +- `Tools/blender/theatre_trim_build.py` builds the El Capitan Theatre's signature features in **world coordinates**: marquee canopy 12.0 × 2.60 m at a 4.60 m soffit, illuminated fascia, recessed soffit, vertical blade sign 1.30 × 5.20 m with frame and bracket. `Tools/unreal/ue_stage_theatre_trim.py` imports to `/Game/Hollywood/Theatre` and stages one actor per piece (single spawn at the origin — no placement arithmetic). - **Two placement bugs, both invisible in a render:** 1. **Axis sign.** Blender +Y is Unreal −Y, so a face at Unreal Y +1001 is Blender Y −10.01. Using +10.01 put the whole marquee on the far side of the boulevard. 2. **Absolute Z.** Trim heights were absolute while the pavement at the theatre is 4.56 m above the project datum, so the canopy hung 5 cm above the pavement — the QA wall probe caught it as a knee-height blocker. `GROUND_Z` now offsets every height. @@ -436,7 +436,7 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. - Fluting: 20 grooves per column as thin shadow-tone cylinders set into the shaft (120 pieces) rather than boolean cuts — the eye reads them in silhouette and grazing light, booleans would cost far more for that. Motto band is now a real recess: surround 6 cm proud, panel 12 cm behind (was an 8 cm dark stripe). Hero 148 parts/3 044 tris -> 271/5 492, still 14.50 m. - **The real find:** `ue_hero_facade_import.py` had its own hero position `(-12024.5, -1450, 0)` — the pre-`0.1.0` value, before the frontage placement fix moved the hero to `(-13336, +1307)`. Running the importer on its own therefore teleported the hero **13 m east and onto the north side of the boulevard**, and nothing complained: a displaced building still renders. The only symptom was the reference score dropping 0.69 -> 0.56 with the 3x3 composition correlation flipping +0.54 -> -0.08. -- Fix: `Tools/ue_placement.py` is now the single source of truth (hero position, verified frontage spans, `span_error`/`describe_span_failure`). Both `ue_hero_facade_import.py` and `ue_build_demo_map.py` import it, the import asserts the staged span and logs `PLACEMENT ERROR` when it does not match, and tests assert the stale literal cannot come back. Spawn-view score back to **0.72** (best yet). +- Fix: `Tools/unreal/ue_placement.py` is now the single source of truth (hero position, verified frontage spans, `span_error`/`describe_span_failure`). Both `ue_hero_facade_import.py` and `ue_build_demo_map.py` import it, the import asserts the staged span and logs `PLACEMENT ERROR` when it does not match, and tests assert the stale literal cannot come back. Spawn-view score back to **0.72** (best yet). - Lesson to keep: **duplicated placement constants drift silently.** The metric that caught it was the composition correlation, not a visual review. ## 2026-11-09 — 1440p re-measured with the kit: 363 fps, ~12% cost @@ -447,7 +447,7 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-09 — 604 unmaterialed actors: material rebuild was delete+recreate -- The "blockout cleaning pass" turned out to need **material and placement hygiene, not more geometry**. `Tools/ue_audit_scene.py` (new) found **604 of 620 mesh actors with no material**: kit pieces and theatre trim all untextured. +- The "blockout cleaning pass" turned out to need **material and placement hygiene, not more geometry**. `Tools/unreal/ue_audit_scene.py` (new) found **604 of 620 mesh actors with no material**: kit pieces and theatre trim all untextured. - **Cause:** `ensure_material` in `ue_build_demo_map.py` deleted and recreated each `/Game/Hollywood/Site/M_*` asset every run ("so a look change cannot be masked by a stale asset"). That orphane every reference: actors staged by other tools pointed at deleted assets and fell back to the default material. The map looked only _slightly_ flat — no error, no warning. - **Fix:** update the material **in place** — load it, `MaterialEditingLibrary. delete_all_material_expressions(mat)`, rebuild the graph, recompile, save. Note `Material.expressions` is protected from Python; use the library's deleter. - **Ordering rule now documented and enforced:** the demo builder rebuilds materials, so kit and theatre staging must run _after_ it. `render_screenshots.sh` now runs demo map → kit assembly → theatre trim → scene audit → shot cameras → capture, and **fails on a non-zero audit count**. @@ -462,7 +462,7 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-09 — Retail frontage and two pipeline traps -- **`Tools/render_screenshots.sh` called `./Engine/...` for the capture step** while every commandlet above went through `run_cmdlet`'s `cd "$ENGINE"`. With `set -e` the script died at exit 127 _after_ rebuilding the maps, so the PNGs silently stayed a day old and `--verify` never ran. Fixed to `"$ENGINE/Engine/Binaries/..."`. Symptom to remember: maps rebuild, screenshots do not move. +- **`Tools/review/render_screenshots.sh` called `./Engine/...` for the capture step** while every commandlet above went through `run_cmdlet`'s `cd "$ENGINE"`. With `set -e` the script died at exit 127 _after_ rebuilding the maps, so the PNGs silently stayed a day old and `--verify` never ran. Fixed to `"$ENGINE/Engine/Binaries/..."`. Symptom to remember: maps rebuild, screenshots do not move. - **The generic façade kit is applied to every actor labelled `Frontage_*` or `North_*`**, so staging a hand-modelled façade under those labels gets the kit pasted over it — two contradicting façade languages on one wall. `ue_apply_facade_kit.py` now skips `MODELLED` labels, and retail trim stages as `RetailTrim_*`. - **A Blender builder cannot be imported by a unit test** (`import bpy` at module scope). Pure layout arithmetic lives in `Tools/_common.py`; `Tools/tests/test_retail_frontage.py` tests it there, and the builder delegates. Same trick `fbx_export_preset.py` uses for its preset dicts. - **Joining 85 parts into one mesh per building** before export cut the Unreal import from 85 assets to 2, and the staging step to two spawns. Record per-part dimensions _before_ the join: after it only the joined bounds exist. @@ -483,14 +483,14 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. - **The real reason the character pose never changed across a dozen rebuilds:** the FBX importer reported success every time, but `Character_Anim` and its two AnimSequences kept their original data. Proof: after changing the arm base from 1.55 rad to 0.55 rad and rebuilding, `AnimPose.get_bone_pose("upperarm01_L")` returned the **identical** quaternion `(+0.006, +0.002, -0.179, -0.984)` as before. An asset that does not change when its source does is not being written. - `AssetImportTask.replace_existing = True` was set, and `EditorAssetLibrary.delete_asset` returned **False** for both clips, even after `chmod u+w` (the `.uasset` files are LFS-lockable and check out read-only). Deleting is blocked because the pawn's C++ constructor hard-references the clips. -- **Workaround that works: import under a new name.** `Tools/ue_character_anim_import.py` now uses `PREFIX = "CharacterV2"`, which creates `CharacterV2`, `CharacterV2_Skeleton` and `CharacterV2Human_rig_GigLine_{Idle,Walk}` fresh; the same quaternion probe then returned `(+0.439, +0.898)`, i.e. the 0.55 rad arm rotation had finally landed. +- **Workaround that works: import under a new name.** `Tools/unreal/ue_character_anim_import.py` now uses `PREFIX = "CharacterV2"`, which creates `CharacterV2`, `CharacterV2_Skeleton` and `CharacterV2Human_rig_GigLine_{Idle,Walk}` fresh; the same quaternion probe then returned `(+0.439, +0.898)`, i.e. the 0.55 rad arm rotation had finally landed. - **Check the asset, not the importer.** A pose or material change that does not alter the probed asset data did not happen, whatever the log says. Probing a clip's bone quaternion takes one commandlet and settles it in seconds. -- **The arms-down rotation belongs in the clips, not in the bind.** Every bind experiment (bake pose to rest, clear shape keys, apply the armature modifier, re-bind) was either impossible on this shape-keyed MPFB mesh or left Unreal showing the old bind. `Tools/character_anim_build.py` now carries a measured base offset in the clip keys (`BASE`: 0.55 rad on `upperarm01`, chosen by sweeping the rotation in Blender and reading the bone direction), and the character stands with its arms at its sides. +- **The arms-down rotation belongs in the clips, not in the bind.** Every bind experiment (bake pose to rest, clear shape keys, apply the armature modifier, re-bind) was either impossible on this shape-keyed MPFB mesh or left Unreal showing the old bind. `Tools/blender/character_anim_build.py` now carries a measured base offset in the clip keys (`BASE`: 0.55 rad on `upperarm01`, chosen by sweeping the rotation in Blender and reading the bone direction), and the character stands with its arms at its sides. ## 2026-11-09 — Textures: ComfyUI works, Stable Diffusion fails for flat swatches -- **ComfyUI is reachable at `127.0.0.1:8188` (0.44.0)** and its API is the clean way in: `POST /prompt` with an API-format graph (`[node_id, output_index]` references — the UI's workflow JSON cannot be posted), then poll `/history/` and fetch `/view?filename=...`. `Tools/comfyui_generate.py` does this and writes a sidecar with model, prompt, seed and the full graph. Installed checkpoints: `juggernautXL_ragnarok.safetensors`, `epicrealismXL_pureFix.safetensors`, `sd-v1-5-inpainting.ckpt`, plus inpainting variants. -- **Do not ask Stable Diffusion for a flat texture swatch.** Three attempts at "seamless skin texture" produced an anatomical close-up twice and a leather-scale surface once. SDXL has no reliable notion of a material sample without anatomy, so every seed needs manual rejection — worse than noise we control. Procedural generation won for skin pores (`Tools/generate_character_textures.py`, tileable octave noise) and a strand sheet for hair. +- **ComfyUI is reachable at `127.0.0.1:8188` (0.44.0)** and its API is the clean way in: `POST /prompt` with an API-format graph (`[node_id, output_index]` references — the UI's workflow JSON cannot be posted), then poll `/history/` and fetch `/view?filename=...`. `Tools/gen/comfyui_generate.py` does this and writes a sidecar with model, prompt, seed and the full graph. Installed checkpoints: `juggernautXL_ragnarok.safetensors`, `epicrealismXL_pureFix.safetensors`, `sd-v1-5-inpainting.ckpt`, plus inpainting variants. +- **Do not ask Stable Diffusion for a flat texture swatch.** Three attempts at "seamless skin texture" produced an anatomical close-up twice and a leather-scale surface once. SDXL has no reliable notion of a material sample without anatomy, so every seed needs manual rejection — worse than noise we control. Procedural generation won for skin pores (`Tools/gen/generate_character_textures.py`, tileable octave noise) and a strand sheet for hair. - **Unreal collapses the second material slot on a skeletal-mesh import.** A joined hair cap (Blender shows two slots, hair faces on index 1) arrives as a single slot named after the body material, so the hair renders with the skin material. `unreal.SkeletalMesh` has no `set_material`, so it cannot be re-slotted after import. A separate skeletal mesh attached as a second component is the way around it. ## 2026-11-09 — Hair as a separate mesh, and a material-editing crash @@ -502,7 +502,7 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-09 — Character blueprints and how to compare a render to one - **A SDXL character sheet is reliable**: "full body front view, neutral pose, plain grey background, character sheet" gives a usable turnaround with fixed seeds. Four views (front/side/back/head) from one description is enough to evaluate against; the sheet and its JSON live in `Art/characters/blueprints/`. -- **Segmenting the character out of a review shot does not work by colour.** The sky band, the pale wall and the dark pavement are each larger and more saturated than the body, so every size-based rule picked a backdrop blob and the first report measured the whole frame. `Tools/character_match.py` now takes an explicit `--render-box`; the proper fix is a background plate rendered without the character. +- **Segmenting the character out of a review shot does not work by colour.** The sky band, the pale wall and the dark pavement are each larger and more saturated than the body, so every size-based rule picked a backdrop blob and the first report measured the whole frame. `Tools/blender/character_match.py` now takes an explicit `--render-box`; the proper fix is a background plate rendered without the character. - **The report has to separate the failures it measures.** Aspect, fill, coverage and two colour distances are reported apart on purpose: an absolute similarity number between a photoreal sheet and a low-detail body would mean nothing. The first real comparison (aspect Δ1.24, mean ΔE 31.9) was dominated by the model being **unclothed** while the blueprint wears a tee and jeans. - **`SetMaterial(0, ...)` in the pawn constructor does not visibly land** on the body mesh: `M_Character_Skin` is built and assigned, yet the render stays neutral grey. Either the slot does not exist yet at that point or the assignment is dropped; `BeginPlay` is the next thing to try. - **MPFB vertex groups are polluted by helper geometry.** The body mesh weights its helper cubes to groups named `Left`, `Right`, `HelperGeometry` and `JointCubes` at 1.0, so a "strongest group" lookup returns a helper for most vertices - the first clothing build selected **zero** faces. Filter helper names out before asking which bone a vertex belongs to. @@ -515,9 +515,9 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. ## 2026-11-09 — The sidewalk does not block, and why -- **The staged `Sidewalk_North` mesh does not stop a trace or a capsule.** It carries `QueryAndPhysics`, its bbox tops out at Z 380, and `Tools/ue_import_site.py` sets `CTF_USE_SIMPLE_AND_COMPLEX` with `auto_generate_collision=True` — yet both a downward trace and a falling capsule pass straight through to `Road_Hollywood` at 366. +- **The staged `Sidewalk_North` mesh does not stop a trace or a capsule.** It carries `QueryAndPhysics`, its bbox tops out at Z 380, and `Tools/unreal/ue_import_site.py` sets `CTF_USE_SIMPLE_AND_COMPLEX` with `auto_generate_collision=True` — yet both a downward trace and a falling capsule pass straight through to `Road_Hollywood` at 366. - **Likely cause: the auto-generated simple collision for a 279 m x 8.6 m x 3.8 m slab is degenerate.** A convex hull over that aspect ratio either collapses or ends up far from the surface; the complex side is one-sided and the mesh may be wound the wrong way. `bOneSidedGeometry` no longer exists on `BodySetup` in 5.8, so the flag cannot be flipped from Python. -- **Working stopgap:** an invisible `279 x 8.6 x 0.08 m` blocking cube with its top at Z 380 in `Tools/ue_build_demo_map.py`. Verified: `top surface under player: Z=380 mesh=Cube`, `foot: actor Z 470, feet 382`. +- **Working stopgap:** an invisible `279 x 8.6 x 0.08 m` blocking cube with its top at Z 380 in `Tools/unreal/ue_build_demo_map.py`. Verified: `top surface under player: Z=380 mesh=Cube`, `foot: actor Z 470, feet 382`. - **The real fix belongs in the site export:** split the sidewalk into segments of a few metres, or export explicit `UCX_` boxes, so the collision is a set of sane hulls rather than one degenerate one. ## 2026-11-09 — Slicing the sidewalk did not fix its collision @@ -597,8 +597,8 @@ Road_Hollywood_07 158 (was 366) ## 2026-11-10 — `chmod u+w` on the Blender files, and what re-rendering does not do -- Every `.blend` under `Art/` checked out read-only; a Blender save then fails with `RuntimeError: Error: Cannot save blend file ... is not writable`, and because the export happens after the save in `Tools/hero_facade_build.py`, **the FBX is never written and the old one is reused silently**. All 20 `.blend`/`.blend1` files were made writable; `find Art -name '*.blend' ! -perm -u+w` now returns 0. -- **Re-rendering does not pick up `.blend` changes.** `Tools/render_screenshots.sh` renders the imported UE assets, so a rebuild is needed in between: Blender build → `Tools/ue_*_import.py` → render. Chmod alone cannot change a pixel; the hero captures are byte-identical before and after. +- Every `.blend` under `Art/` checked out read-only; a Blender save then fails with `RuntimeError: Error: Cannot save blend file ... is not writable`, and because the export happens after the save in `Tools/blender/hero_facade_build.py`, **the FBX is never written and the old one is reused silently**. All 20 `.blend`/`.blend1` files were made writable; `find Art -name '*.blend' ! -perm -u+w` now returns 0. +- **Re-rendering does not pick up `.blend` changes.** `Tools/review/render_screenshots.sh` renders the imported UE assets, so a rebuild is needed in between: Blender build → `Tools/ue_*_import.py` → render. Chmod alone cannot change a pixel; the hero captures are byte-identical before and after. ## 2026-11-10 — skills: one common copy, project-specific stays local diff --git a/Screenshots/README.md b/Screenshots/README.md index 904e1ad..e65476c 100644 --- a/Screenshots/README.md +++ b/Screenshots/README.md @@ -5,7 +5,7 @@ Two reviews in one file: - **ReviewMap** stages imported assets on their own — individual objects, the Blender import, the camera angles and the lighting comparison. Nothing from the game map is involved. - **DemoMap** is the actual game map as it is being developed: the street, the buildings, the player and the vehicle in their real placement. -Both capture passes are produced by [`Tools/render_screenshots.sh`](../Tools/render_screenshots.sh); the asset renders come from [`Tools/blender_screenshots.py`](../Tools/blender_screenshots.py). +Both capture passes are produced by [`Tools/review/render_screenshots.sh`](../Tools/render_screenshots.sh); the asset renders come from [`Tools/blender/blender_screenshots.py`](../Tools/blender_screenshots.py). ## Status @@ -45,10 +45,10 @@ Both capture passes are produced by [`Tools/render_screenshots.sh`](../Tools/ren ### Blender vs in-game -Regenerated by `Tools/build_comparison_md.py`; everything between the markers is rewritten, so edit outside them. +Regenerated by `Tools/review/build_comparison_md.py`; everything between the markers is rewritten, so edit outside them. -- Captures: `Screenshots/review/` and `Screenshots/demo/` (23 files) -- Blender renders: `Screenshots/blender/` (22 files), made by `Tools/blender_screenshots.py` +- Captures: `Screenshots/review/` and `Screenshots/demo/` (26 files) +- Blender renders: `Screenshots/blender/` (22 files), made by `Tools/blender/blender_screenshots.py` Left is what the engine draws, right is what Blender has. A gap between them is the import problem, not a taste problem: scale, material colour, missing parts and split systems all show up here before they show up in a review. @@ -56,6 +56,9 @@ Left is what the engine draws, right is what Blender has. A gap between them is | In-game | Blender | Shot | | --- | --- | --- | +| | — | `hero-compare-facade-front.png` | +| | — | `hero-compare-front.png` | +| | — | `hero-compare-oblique.png` | | | | `review-character-feet.png` | | | | `review-character.png` | | | — | `review-ground-contact.png` | @@ -80,7 +83,7 @@ Left is what the engine draws, right is what Blender has. A gap between them is | | | `review-demo-vehicle.png` | | | — | `review-demo-wheel.png` | -7 of 23 in-game captures have a Blender counterpart; the rest are street and building shots whose meshes come from the Geo pipeline rather than a .blend. +7 of 26 in-game captures have a Blender counterpart; the rest are street and building shots whose meshes come from the Geo pipeline rather than a .blend. Approval and issue notes are recorded only in the status table at the top of this file. @@ -246,10 +249,10 @@ What it shows: the arms hang down with a soft elbow bend, the hands sit at hip h ## The iteration loop (uncommitted) -`Tools/visual_loop.py` measures each capture against its reference photograph and turns the deltas into modelling actions for the Blender skill packs: +`Tools/review/visual_loop.py` measures each capture against its reference photograph and turns the deltas into modelling actions for the Blender skill packs: ```bash -python3 Tools/visual_loop.py --compare --iteration 3 # writes Art/reference/iterations/3/ +python3 Tools/review/visual_loop.py --compare --iteration 3 # writes Art/reference/iterations/3/ ``` That directory is **gitignored**: iterations stay uncommitted until a shot is confirmed, and only confirmed shots are promoted into `Screenshots/demo/`. The feedback file names the affected skill (`blender-modeler`, `archviz`, `materials`, `lookdev`, `camera-cinematography`) so the next modelling pass has a starting point rather than an opinion. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 760ea78..931a3d2 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -31,7 +31,7 @@ Reference photographs are **not** part of this repository. They live in the giti | Local folder | Source | Provenance | | --- | --- | --- | | `Art/reference/hollywood/` | Copied 2026-11-06 from `…/06-games/security-game/docs/screens/refs/hollywood/` | That folder's `SIDECAR.md` (author, licence, URL, crop notes) | -| `Art/reference/north-chinese/` | Fetched 2026-11-06 with `Tools/commons_fetch.py` from the Wikimedia Commons API | `sidecar.json` in the folder | +| `Art/reference/north-chinese/` | Fetched 2026-11-06 with `Tools/gen/commons_fetch.py` from the Wikimedia Commons API | `sidecar.json` in the folder | | `Art/reference/north-tussauds/` | Same | `sidecar.json` in the folder | Reference notes and images are local only and excluded from version control. @@ -46,9 +46,9 @@ Rules that still apply, because they are about shipped output rather than local | Component | Source | Licence | Status | | --- | --- | --- | --- | -| Character mesh, standard rig, and baked skin | MPFB 2.0.18 bundled base mesh, rig, and textures; `Tools/mpfb_character_export.py` | CC0 1.0 for bundled assets | [x] against the [versioned MPFB licence](https://github.com/makehumancommunity/mpfb2/blob/v2.0.18/LICENSE.md); imported character assets and Blender source are tracked. | +| Character mesh, standard rig, and baked skin | MPFB 2.0.18 bundled base mesh, rig, and textures; `Tools/blender/mpfb_character_export.py` | CC0 1.0 for bundled assets | [x] against the [versioned MPFB licence](https://github.com/makehumancommunity/mpfb2/blob/v2.0.18/LICENSE.md); imported character assets and Blender source are tracked. | | OSM blockout | OSM extract and Blosm-generated geometry | OpenStreetMap data, local private use | Source manifests in `Geo/manifests/`. | | Terrain and street prototype | USGS 3DEP DEM, OSM data, project scripts | Public domain USGS data; OpenStreetMap data, local private use | Source manifests in `Geo/manifests/`. | -| Hero façade blockout | Project-owned geometry from `Tools/hero_facade_build.py`, dimensions from OSM and the reference photography | Project-owned | Committed with a per-part evidence report (`Art/blender/hero/hero-facade-*.json`) and a position sidecar (`hero-facade.geo.json`). | +| Hero façade blockout | Project-owned geometry from `Tools/blender/hero_facade_build.py`, dimensions from OSM and the reference photography | Project-owned | Committed with a per-part evidence report (`Art/blender/hero/hero-facade-*.json`) and a position sidecar (`hero-facade.geo.json`). | | Street props (candidate reuse) | Sibling project `…/06-games/security-game`, `assets/models/blender_src/*.blend` | Project-owned by the same author | **Not yet imported.** Sources copied to `Art/props/blender_src/` with `Art/props/reuse-manifest.json`; a licence statement is required before any of them ship. | | Hero façade stone albedo | `Art/textures/generated/T_FacadeAshlar_AI.png`, generated locally with ComfyUI (Stability Matrix, `/home/zwitschi/StabilityMatrix/Data/README.md`) using **AlbedoBase XL v3.1-Large** at seed 21, workflow `texture_albedobase_xl` | Civitai model 140737 — commercial image use allowed, credit required | [ ] credit line still to be added to the game's credits document before release; sidecar `T_FacadeAshlar_AI.json` records model, prompt, seed and graph | diff --git a/TODO.md b/TODO.md index 0bbb84c..e100e0d 100644 --- a/TODO.md +++ b/TODO.md @@ -61,7 +61,7 @@ DemoMap, 60 m off the street: Z 291 -> 467 -> 920 -> 1343 vZ +1595 DemoMap, 25 m above ground: Z 2496 -> 2664 -> 3068 -> 3434 vZ +1509 launches at once ``` -**So the Chaos rig is fine and `DemoMap` is what breaks it** - at any height, anywhere in that map, and not by contact (25 m in the air launches immediately). The next comparison is between the two maps' world settings and level actors: `ReviewMap` was built by `Tools/ue_build_review_map.py`, `DemoMap` by `Tools/ue_build_demo_map.py`, and the difference that matters is in what one stages and the other does not. +**So the Chaos rig is fine and `DemoMap` is what breaks it** - at any height, anywhere in that map, and not by contact (25 m in the air launches immediately). The next comparison is between the two maps' world settings and level actors: `ReviewMap` was built by `Tools/unreal/ue_build_review_map.py`, `DemoMap` by `Tools/unreal/ue_build_demo_map.py`, and the difference that matters is in what one stages and the other does not. ### The transform is written directly, not moved @@ -77,7 +77,7 @@ The actor keeps rising at ~1200 cm/s while its velocity is pinned to zero. **Som ### Map-content bisect: the terrain is involved -Built `Tools/ue_vehicle_probe_map.py` (duplicates `ReviewMap`, optionally adds ONE demo mesh plus the pawn, saves as `VehicleProbe`) and ran it: +Built `Tools/unreal/ue_vehicle_probe_map.py` (duplicates `ReviewMap`, optionally adds ONE demo mesh plus the pawn, saves as `VehicleProbe`) and ran it: ``` control (no extra mesh) Z 300 -> 297 over 1.25 s (resting on the review floor) @@ -86,7 +86,7 @@ control (no extra mesh) Z 300 -> 297 over 1.25 s (resting on the review fl So `Terrain_DEM` is enough to reproduce it in a map that otherwise works. Two fixes were then tried and **neither helped**: -- `Tools/ue_clear_simple_collision.py` cleared the auto-generated convex hulls from all 76 site/street meshes (terrain had **1 convex element** over the whole 279 m) and set complex-as-simple - still rises. +- `Tools/unreal/ue_clear_simple_collision.py` cleared the auto-generated convex hulls from all 76 site/street meshes (terrain had **1 convex element** over the whole 279 m) and set complex-as-simple - still rises. - Setting the terrain component to `CollisionEnabled::QueryOnly` so traces still hit it but the physics solver ignores it - still rises. That means the ejection is not the convex hull and not the collision channel; the next probe is whether it is the _complex_ triangles (inverted winding pushing bodies out) - test by setting `Terrain_DEM` to **no collision at all** in the probe map and confirming the pawn falls. @@ -103,7 +103,7 @@ The pawn still rises in every case, so the fault is not the terrain's convex hul Deferred rather than guessed. What a fresh session needs: -- the probe workflow is in place: `Tools/ue_vehicle_probe_map.py` with `GIGLINE_PROBE_ASSET` and `GIGLINE_PROBE_NOCOLLISION`, `Tools/ue_clear_simple_collision.py` for the hull hygiene; +- the probe workflow is in place: `Tools/unreal/ue_vehicle_probe_map.py` with `GIGLINE_PROBE_ASSET` and `GIGLINE_PROBE_NOCOLLISION`, `Tools/unreal/ue_clear_simple_collision.py` for the hull hygiene; - the vehicle diagnostics are switches: `-probes`, `-profile`, `-novehmover`, plus the always-on velocity clamp, overlap list and velocity profile; - the next question is _why a map carrying a 279 m static mesh changes a physics body's behaviour when that mesh has no collision at all_, which points at streaming, memory layout or the physics scene's broadphase rather than at collision authoring. @@ -152,7 +152,7 @@ imported mesh bounds y = -13 .. +1250 cm The facade's detail - banners, and by the same construction the window bays and portal surrounds - is placed **behind** the wall's front face, so it is inside `Hero_Mass` and never renders. That is why the captures show a plain block with six columns: the columns, cornice and dentils happen to protrude, everything else does not. The model does not need remodelling first; its detail needs moving in front of the wall. -**Fix:** give `Tools/hero_facade_build.py` a `FRONT_Y = -BODY_D / 2.0 - 0.5` constant (the mass's front face) and place every elevation detail relative to it (`FRONT_Y - 0.10` for applied detail, `FRONT_Y` for flush), instead of the hand-typed negative coordinates each call site uses now. Then re-export, re-import and re-render. +**Fix:** give `Tools/blender/hero_facade_build.py` a `FRONT_Y = -BODY_D / 2.0 - 0.5` constant (the mass's front face) and place every elevation detail relative to it (`FRONT_Y - 0.10` for applied detail, `FRONT_Y` for flush), instead of the hand-typed negative coordinates each call site uses now. Then re-export, re-import and re-render. **And check the review shot.** The three hero captures show a plain block with six columns and nothing else - no banners, no windows, no portals - while the mesh demonstrably carries all four materials (`M_Facade_Banner`, `M_Facade_Stone`, `M_Facade_StoneShadow`, `M_Facade_GlassDark`) and is staged at `(-13336, 1307, 0)` with yaw 0. That suggests the cameras are framing the wrong actor (`Frontage_SM_ElCapitanTheatre` sits at the origin) or the wrong side, and the "rework" note may be describing a camera error rather than the model. Verify that first: one render with a camera aimed at `Hero_Facade` from Y -4000 at 8 m height. @@ -171,7 +171,7 @@ Still missing, from the same references: warm sandstone instead of the flat pale ### The iteration loop -`bash Tools/hero_iterate.sh` runs the whole chain in one command: chmod the Blender files, build the facade, import it, render the capture set, judge the shot with the vision model, and write a comparison plate at `Screenshots/review/hero-compare-front.png`. Each step fails loudly and logs to `/tmp/gigline/`. +`bash Tools/review/hero_iterate.sh` runs the whole chain in one command: chmod the Blender files, build the facade, import it, render the capture set, judge the shot with the vision model, and write a comparison plate at `Screenshots/review/hero-compare-front.png`. Each step fails loudly and logs to `/tmp/gigline/`. Iteration 1 verdict (judge, confidence 0.9): @@ -181,7 +181,7 @@ Measured against the plate, that splits into: | Finding | Reality | Next action | | --- | --- | --- | -| not warm sandstone | true - the UE material `M_FacadeStone` is still near-white (0.62-0.76 grey) | warm the UE material spec in `Tools/ue_hero_facade_import.py`, not the Blender one | +| not warm sandstone | true - the UE material `M_FacadeStone` is still near-white (0.62-0.76 grey) | warm the UE material spec in `Tools/unreal/ue_hero_facade_import.py`, not the Blender one | | fluting missing | false - 120 flute strips exist and cast shadows; they are lost at 45 m | not a modelling problem; check the review camera distance | | banners wrong | partly - proportions are now 1.35 x 6.2 m and hang correctly, the lettering is absent | needs a texture, not geometry | | no doorways | false - three 1.2 m deep recesses with steps; they read dark because the reveal is glass-black | lighten the reveal material | @@ -199,7 +199,7 @@ Measured against the plate, that splits into: ### Vehicle production - active - session-5598ce7e-27e3-4c64-bb49-c04a4a31ee58 (branch `vehicle-0.3.0`) -- [A] Model one vehicle in Blender with collision meshes; export to Unreal. **Model done** (`Tools/vehicle_build.py` → `Art/blender/vehicle/gigline-sedan.blend`, preview in `Art/blender/vehicle/preview.png`): 4.72 x 1.80 x 1.44 m, 2.78 m wheelbase, 1.62 m track, 0.33 m wheels, 252 triangles, four `UCX_` convex hulls. Dimensions come from real sedan figures and are logged to the report, so the vehicle config can read them rather than restating them. - **Imported into Unreal** (`Tools/ue_vehicle_import.py` → `/Game/Hollywood/Vehicle`), split per the vehicle-artist object split: `SM_Vehicle_Body` (3 slots, 4.76 x 1.76 x 1.28 m), `SM_Vehicle_Glass`, `SM_Vehicle_Light_Front`, `SM_Vehicle_Light_Rear`, `SM_Vehicle_Mirrors`, and one `SM_Vehicle_Wheel` (0.22 x 0.66 x 0.66 m) reused four times. The body FBX carries the `UCX_` hulls as simple collision. +- [A] Model one vehicle in Blender with collision meshes; export to Unreal. **Model done** (`Tools/blender/vehicle_build.py` → `Art/blender/vehicle/gigline-sedan.blend`, preview in `Art/blender/vehicle/preview.png`): 4.72 x 1.80 x 1.44 m, 2.78 m wheelbase, 1.62 m track, 0.33 m wheels, 252 triangles, four `UCX_` convex hulls. Dimensions come from real sedan figures and are logged to the report, so the vehicle config can read them rather than restating them. - **Imported into Unreal** (`Tools/unreal/ue_vehicle_import.py` → `/Game/Hollywood/Vehicle`), split per the vehicle-artist object split: `SM_Vehicle_Body` (3 slots, 4.76 x 1.76 x 1.28 m), `SM_Vehicle_Glass`, `SM_Vehicle_Light_Front`, `SM_Vehicle_Light_Rear`, `SM_Vehicle_Mirrors`, and one `SM_Vehicle_Wheel` (0.22 x 0.66 x 0.66 m) reused four times. The body FBX carries the `UCX_` hulls as simple collision. - **LODs cut for real:** decimating the duplicate gives LOD1 at 38 triangles and LOD2 at 14, against 132 for LOD0. The first attempt duplicated the shell unchanged and produced 84-triangle LODs that saved nothing - worth remembering as the failure mode of a "LOD" step nobody measures. - **LOD group wiring is open:** `StaticMeshEditorSubsystem.import_lod` is unavailable in a headless commandlet (`get_editor_subsystem` returns None), so the LOD meshes import as their own assets and the group still needs an editor session. - [~] Set up Chaos Vehicle with wheel and physics configuration. **It sits on the road now; it does not drive yet.** @@ -207,7 +207,7 @@ Measured against the plate, that splits into: - **Drive test fails, with a specific cause:** `-drivetest` holds full throttle for 3 s and reports speed and distance. Result: `speed=0 cm/s, travelled 2.2 m -> FAIL`, and BeginPlay logs **`0 wheel objects after register`** while the component reports 4 wheel setups. The setups exist but no wheels are created from them, so there is nothing to apply torque to. - **Next:** confirm the skeleton's bone names match the setups (`Wheel_FL`, `Wheel_FR`, `Wheel_RL`, `Wheel_RR`) - a name mismatch silently produces zero wheels - and give the front and rear pairs their own `UChaosVehicleWheel` subclasses rather than the base class, which is what the setups point at today. - **Also open:** LOD group wiring, enter/exit, driving route, handling. -- [x] Fix the vehicle not rendering in the scene. **Chased to a strong clue, not yet solved.** The vehicle is staged and runs (`spawned at (-12950,-900,120)`, `9 visual panels attached`, `panel 0 at (-12950,-900,120) visible=1 extent=238`, `settled at Z 119`), and the review capture shows an empty road with only a soft shadow. `Tools/visual_loop.py --verify` passes the frame; `Tools/judge_screenshot.py` returns `verdict=no confidence=1.0`. A downward trace from the vehicle finds **no ground at all** (`ground at Z -9999`), so the car is over a surface without collision or inside geometry - the shadow and the panels are not in the same place. **Fixed 2026-11-09** — the car renders, built from the owner's `hot hatch.blend`. **Fixed:** the vehicle was spawned 2.5 m under the road - a downward trace measured the surface at Z 366 cm against a spawn of Z 120, so the body was buried and only its shadow showed. The spawn is now Z 470 and it settles there. +- [x] Fix the vehicle not rendering in the scene. **Chased to a strong clue, not yet solved.** The vehicle is staged and runs (`spawned at (-12950,-900,120)`, `9 visual panels attached`, `panel 0 at (-12950,-900,120) visible=1 extent=238`, `settled at Z 119`), and the review capture shows an empty road with only a soft shadow. `Tools/review/visual_loop.py --verify` passes the frame; `Tools/review/judge_screenshot.py` returns `verdict=no confidence=1.0`. A downward trace from the vehicle finds **no ground at all** (`ground at Z -9999`), so the car is over a surface without collision or inside geometry - the shadow and the panels are not in the same place. **Fixed 2026-11-09** — the car renders, built from the owner's `hot hatch.blend`. **Fixed:** the vehicle was spawned 2.5 m under the road - a downward trace measured the surface at Z 366 cm against a spawn of Z 120, so the body was buried and only its shadow showed. The spawn is now Z 470 and it settles there. - **Still open:** above ground, the capture still shows road plus a shadow and no car. Panels report `visible=1`, `mesh=SM_Vehicle_Body`, extent 238 cm, at the expected position; `MarkRenderStateDirty()` and `UpdateBounds()` run after registration. Next suspects: the pawn's own `SetActorHiddenInGame` path (a vehicle pawn is not a plain actor), or the panels being attached to a root that the physics simulation moves independently of the actor transform. - [x] Fix the character and car floating. **The measurement was taken at the wrong moment.** The foot probe ran in `BeginPlay`, while the pawn is still at its spawn height and falling, so every trace reported a surface far below it (`ground 366` against an actor at 561) and read as a float that was not there. The probe now runs **three seconds after spawn**, from the capsule's feet, on three channels, and names the actor it hits. Character: the player start at Z 600 left the soles 39 cm in the air (the capsule settles at 561 on this pavement); the start is now the measured 561. Car: the template's lowest geometry is exhaust, so grounding on the whole model put the tyres 4 cm above the origin - it is now grounded on its wheels, staged at Z 342 after the traced road height (366) disagreed with the visible road by ~25 cm. - [x] Fix the vehicle's material colour. The template's `car paint` is a legacy mix-shader graph that the FBX exporter cannot map, so `SM_Vehicle_Body` imported holding `WorldGridMaterial` and rendered black while Blender showed red. Legacy graphs are now converted to Principled before export (saturated colour wins over the grey diffuse next to it), and the staged body panel is assigned `M_Vehicle_HatchPaint`. Judge: `verdict=yes confidence=0.9`. diff --git a/Tools/README.md b/Tools/README.md index 48fe4e6..88e8f79 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -4,6 +4,21 @@ Repeatable helpers that drive Blender, Unreal, and the reference/asset sources. Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side builders live next to their output in `Art/blender/*/`; this folder holds everything that talks to Unreal, the network, or the repository. +## Layout + +Sixty scripts in one directory stopped being navigable, so they are grouped by what they talk to. The folder is also the sandbox boundary: everything in `unreal/` needs the engine, everything in `blender/` needs the Flatpak, and the rest is host Python. + +| Folder | Holds | Needs | +| --- | --- | --- | +| [`unreal/`](unreal/) | Unreal commandlet scripts and the two map builders; `ue_common.py` and `ue_placement.py` are the shared imports | the engine pin from `Docs/toolchain.md` | +| [`blender/`](blender/) | Blender-side builders and exporters, the FBX preset, the MPFB/MakeHuman character pipeline, `blender_screenshots.py` | Blender Flatpak | +| [`review/`](review/) | The visual loop: `render_screenshots.sh`, `judge_screenshot.py`, `hero_compare.py`, `build_comparison_md.py`, `e2e_asset_check.py`, `hero_iterate.sh`, `visual_loop.py`, `visual_targets.json` | engine + optional OpenRouter key | +| [`gen/`](gen/) | Image and texture generation: local ComfyUI driver, deterministic procedural texture writers, reference fetching | local ComfyUI / network | +| [`input/`](input/) | `input_emulation.py`, `input_test.py`, `run_pie.sh` — OS-level input injection | `/dev/uinput` | +| `_common.py`, `tests/` | Shared helpers and the stdlib test suite | none | + +Scripts resolve the repository root as `../..` from their own folder and put the tools root and their siblings on `sys.path`, so `import ue_common` keeps working from `unreal/`. + ## Conventions - One script, one job. Every input is an argument; no interactive prompts. @@ -16,19 +31,19 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b | Tool | Purpose | | --- | --- | | `check-lfs.sh` | Fails if the `lfs` filter is missing or an LFS-tracked file was committed as a plain blob | -| `facade_kit_build.py` | Builds the reusable façade **module kit** at the measured 4.96 m bay: pilaster, sill, lintel, window bay (surround + glazing), shopfront bay (piers, glass, fascia), door bay (jambs, door, threshold), cornice and parapet + coping. Exports each piece with the shared preset plus a JSON part report (dims + evidence per piece). | -| `ue_import_retail_detail.py` | Imports the two retail trim meshes into `/Game/Hollywood/Frontage` with complex-as-simple collision — the trim decorates a solid the player already collides with, so it must not add snag points. Staged by the demo builder as `RetailTrim_*`. | -| `ue_import_facade_kit.py` | Imports the kit into `/Game/Hollywood/Kit/*` as static meshes with complex-as-simple collision; writes `/tmp/gigline/kit-import.txt`. | -| `retail_frontage_build.py` | Retail frontage trim for **6800 Lee Drug** and **6806–6820** (0.1.6): bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806–6820. Built in world coordinates from the blockout bounds, one joined mesh per building, JSON part report with the evidence behind every piece. `6822 Budget` is **not** built — no photograph exists. | -| `retail_frontage_build.py` | Retail frontage trim for **6800 Lee Drug** and **6806–6820** (0.1.6): bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806–6820. Built in world coordinates from the blockout bounds, one joined mesh per building, JSON part report with the evidence behind every piece. `6822 Budget` is **not** built — no photograph exists. | -| `theatre_trim_build.py` | El Capitan Theatre trim: projecting marquee canopy, illuminated fascia, recessed soffit, vertical blade sign with frame and bracket. Built **in world coordinates** (span and pavement height baked in) with a JSON part report, so staging is a single spawn at the origin. | -| `ue_stage_theatre_trim.py` | Imports the trim to `/Game/Hollywood/Theatre` and stages one actor per piece in the DemoMap with site materials; clears a previous run first. | -| `ue_apply_facade_kit.py` | Lays the kit over a building's street face from its own bounding box: bay count = round(width / 4.96), storeys from the height, shopfronts on the ground floor, windows above, pilasters on bay boundaries, cornice and parapet at the top. Clears a previous application first; unique labels per building. | -| `generate_facade_textures.py` | Deterministic stdlib PNG generator for the facade maps (`T_StoneAshlar`, `T_Stucco`, `T_Asphalt`, `T_Concrete`, each `_BC` base colour + `_R` roughness). Pure-zlib writer, hash-based value noise, identical bytes on every run; `--size` and `--out` (`/tmp/gigline/textures` by default). `ue_build_demo_map.py` imports them into `/Game/Hollywood/Textures` and samples them world-aligned. | -| `ue_audit_scene.py` | Gate on the DemoMap: no unmaterialed actor, every verified frontage span within tolerance (`ue_placement`), no duplicate actor labels. Writes `/tmp/gigline/scene-audit.txt`; `render_screenshots.sh --verify` fails on a non-zero failure count. Catches the class of defect that still renders plausibly. | -| `ue_placement.py` | Single source of truth for where staged actors belong in the DemoMap (hero position, verified frontage spans) plus `span_error`/`describe_span_failure` validators. Exists because the hero position was written twice and drifted — the importer kept the pre-`0.1.0` value and moved the hero 13 m east onto the wrong side of the boulevard. No `unreal` import, so it is unit-tested. | -| `ue_common.py` | Shared helpers for the Unreal commandlet scripts: `make_writable()` clears the read-only bit an LFS `.umap` checks out with (otherwise `save_map()` returns False silently), and `require_saved()` makes that failure explicit. | -| `fbx_export_preset.py` | The **single** FBX export preset (`ue_metric_v1` / `ue_character_v1`): metres→centimetres via `FBX_SCALE_NONE`, axis mapping east +X / north −Y / up +Z, `mesh_smooth_type="FACE"` (smoothing groups survive the import). Every Blender exporter imports it instead of copying the argument block; `preset_kwargs()` is bpy-free and unit-tested. | +| [`blender/facade_kit_build.py`](blender/facade_kit_build.py) | Builds the reusable façade **module kit** at the measured 4.96 m bay: pilaster, sill, lintel, window bay (surround + glazing), shopfront bay (piers, glass, fascia), door bay (jambs, door, threshold), cornice and parapet + coping. Exports each piece with the shared preset plus a JSON part report (dims + evidence per piece). | +| [`unreal/ue_import_retail_detail.py`](unreal/ue_import_retail_detail.py) | Imports the two retail trim meshes into `/Game/Hollywood/Frontage` with complex-as-simple collision — the trim decorates a solid the player already collides with, so it must not add snag points. Staged by the demo builder as `RetailTrim_*`. | +| [`unreal/ue_import_facade_kit.py`](unreal/ue_import_facade_kit.py) | Imports the kit into `/Game/Hollywood/Kit/*` as static meshes with complex-as-simple collision; writes `/tmp/gigline/kit-import.txt`. | +| [`blender/retail_frontage_build.py`](blender/retail_frontage_build.py) | Retail frontage trim for **6800 Lee Drug** and **6806–6820** (0.1.6): bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806–6820. Built in world coordinates from the blockout bounds, one joined mesh per building, JSON part report with the evidence behind every piece. `6822 Budget` is **not** built — no photograph exists. | +| [`blender/retail_frontage_build.py`](blender/retail_frontage_build.py) | Retail frontage trim for **6800 Lee Drug** and **6806–6820** (0.1.6): bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806–6820. Built in world coordinates from the blockout bounds, one joined mesh per building, JSON part report with the evidence behind every piece. `6822 Budget` is **not** built — no photograph exists. | +| [`blender/theatre_trim_build.py`](blender/theatre_trim_build.py) | El Capitan Theatre trim: projecting marquee canopy, illuminated fascia, recessed soffit, vertical blade sign with frame and bracket. Built **in world coordinates** (span and pavement height baked in) with a JSON part report, so staging is a single spawn at the origin. | +| [`unreal/ue_stage_theatre_trim.py`](unreal/ue_stage_theatre_trim.py) | Imports the trim to `/Game/Hollywood/Theatre` and stages one actor per piece in the DemoMap with site materials; clears a previous run first. | +| [`unreal/ue_apply_facade_kit.py`](unreal/ue_apply_facade_kit.py) | Lays the kit over a building's street face from its own bounding box: bay count = round(width / 4.96), storeys from the height, shopfronts on the ground floor, windows above, pilasters on bay boundaries, cornice and parapet at the top. Clears a previous application first; unique labels per building. | +| [`gen/generate_facade_textures.py`](gen/generate_facade_textures.py) | Deterministic stdlib PNG generator for the facade maps (`T_StoneAshlar`, `T_Stucco`, `T_Asphalt`, `T_Concrete`, each `_BC` base colour + `_R` roughness). Pure-zlib writer, hash-based value noise, identical bytes on every run; `--size` and `--out` (`/tmp/gigline/textures` by default). `ue_build_demo_map.py` imports them into `/Game/Hollywood/Textures` and samples them world-aligned. | +| [`unreal/ue_audit_scene.py`](unreal/ue_audit_scene.py) | Gate on the DemoMap: no unmaterialed actor, every verified frontage span within tolerance (`ue_placement`), no duplicate actor labels. Writes `/tmp/gigline/scene-audit.txt`; `render_screenshots.sh --verify` fails on a non-zero failure count. Catches the class of defect that still renders plausibly. | +| [`unreal/ue_placement.py`](unreal/ue_placement.py) | Single source of truth for where staged actors belong in the DemoMap (hero position, verified frontage spans) plus `span_error`/`describe_span_failure` validators. Exists because the hero position was written twice and drifted — the importer kept the pre-`0.1.0` value and moved the hero 13 m east onto the wrong side of the boulevard. No `unreal` import, so it is unit-tested. | +| [`unreal/ue_common.py`](unreal/ue_common.py) | Shared helpers for the Unreal commandlet scripts: `make_writable()` clears the read-only bit an LFS `.umap` checks out with (otherwise `save_map()` returns False silently), and `require_saved()` makes that failure explicit. | +| [`blender/fbx_export_preset.py`](blender/fbx_export_preset.py) | The **single** FBX export preset (`ue_metric_v1` / `ue_character_v1`): metres→centimetres via `FBX_SCALE_NONE`, axis mapping east +X / north −Y / up +Z, `mesh_smooth_type="FACE"` (smoothing groups survive the import). Every Blender exporter imports it instead of copying the argument block; `preset_kwargs()` is bpy-free and unit-tested. | | `sibling_asset_audit.py` | Reads the sibling project's prop library from GLB headers into `Art/props/reuse-manifest.json`; copies nothing | | `scale_test_export.py` / `ue_scale_test_import.py` | The 1 m = 100 uu scale test; the named presets `ue_metric_v1` live in these two files | @@ -36,42 +51,44 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b | Tool | Purpose | | --- | --- | -| `commons_fetch.py` | Wikimedia Commons: search, exact `File:` titles, or a whole category; writes a provenance `sidecar.json` | -| `page_image_fetch.py` | Resolves a web page's `og:image` and downloads it (Flickr, Commons file pages, photo blogs) | +| [`gen/commons_fetch.py`](gen/commons_fetch.py) | Wikimedia Commons: search, exact `File:` titles, or a whole category; writes a provenance `sidecar.json` | +| [`gen/page_image_fetch.py`](gen/page_image_fetch.py) | Resolves a web page's `og:image` and downloads it (Flickr, Commons file pages, photo blogs) | | `addon_compat_probe.py` | Verifies MPFB/Blosm can load and generate headless on the pinned Blender | ## Blender content | Tool | Purpose | | --- | --- | -| `hero_facade_build.py` | Parametric hero façade; emits a JSON report naming the evidence behind every part | -| `character_anim_build.py` | Authors the basic animation set on the MPFB rig — `GigLine_Idle` (60 f) and `GigLine_Walk` (30 f) sampled from parametric cycles at 30 fps — and exports one animated FBX with the `ue_character_v1` preset. Rotation axes are measured on the rig, not assumed (probe in the docstring). `--out`, `--report`. | -| `mpfb_character_export.py` | MPFB human, standard rig, baked skin, FBX export | +| [`blender/hero_facade_build.py`](blender/hero_facade_build.py) | Parametric hero façade; emits a JSON report naming the evidence behind every part | +| [`blender/character_anim_build.py`](blender/character_anim_build.py) | Authors the basic animation set on the MPFB rig — `GigLine_Idle` (60 f) and `GigLine_Walk` (30 f) sampled from parametric cycles at 30 fps — and exports one animated FBX with the `ue_character_v1` preset. Rotation axes are measured on the rig, not assumed (probe in the docstring). `--out`, `--report`. | +| [`blender/mpfb_character_export.py`](blender/mpfb_character_export.py) | MPFB human, standard rig, baked skin, FBX export | ## Unreal import and level building | Tool | Purpose | | --- | --- | -| `ue_import_site.py` | Imports the six site FBXs into `/Game/Hollywood/Site`, setting collision complexity per mesh | -| `ue_hero_facade_import.py` | Imports the façade and places it on the measured building line | -| `ue_build_demo_map.py` | Builds `/Game/Hollywood/DemoMap`: lighting, façade, site meshes, player start | -| `hero_iterate.sh` | Full hero-facade loop in one command: chmod the Blender files, build the facade, import it, render the capture set, judge the shot and write a comparison plate. `bash Tools/hero_iterate.sh [shot]`. | -| `hero_compare.py` | Side-by-side plate of a reference photograph and a rendered capture, scaled to one height and labelled. `--reference`, `--render`, `--out`. | -| `ue_clear_simple_collision.py` | Removes the FBX import's auto-generated convex hulls from the site and street meshes and sets them to complex-as-simple, so physics bodies are not pushed out of a 279 m hull. | -| `ue_vehicle_probe_map.py` | Copies `ReviewMap` to `VehicleProbe`, optionally adds one demo mesh and the Chaos pawn, and saves it, to bisect which map content throws a physics body into the air. Set `GIGLINE_PROBE_ASSET` to stage one mesh; unset for the control. | -| `ue_build_review_map.py` | Rebuilds `Test/ReviewMap`, stages the shot camera list, and stages the demo-map views | -| `ue_create_input_assets.py` | Creates `/Game/Input` actions and the mapping context headlessly | -| `ue_character_anim_import.py` | Imports the animated FBX into `/Game/Test/Character` and **verifies an AnimSequence exists** — an FBX exported without `bake_anim` imports a mesh and no clips, with no error. Writes `VERDICT: PASS/FAIL` to `/tmp/gigline/character-anim-import.txt`. | -| `ue_character_import.py` | Skeletal import plus the material/texture chain Interchange skips | +| [`unreal/ue_import_site.py`](unreal/ue_import_site.py) | Imports the six site FBXs into `/Game/Hollywood/Site`, setting collision complexity per mesh | +| [`unreal/ue_hero_facade_import.py`](unreal/ue_hero_facade_import.py) | Imports the façade and places it on the measured building line | +| [`unreal/ue_build_demo_map.py`](unreal/ue_build_demo_map.py) | Builds `/Game/Hollywood/DemoMap`: lighting, façade, site meshes, player start | +| [`review/hero_iterate.sh`](review/hero_iterate.sh) | Full hero-facade loop in one command: chmod the Blender files, build the facade, import it, render the capture set, judge the shot and write a comparison plate. `bash Tools/hero_iterate.sh [shot]`. | +| [`review/hero_compare.py`](review/hero_compare.py) | Side-by-side plate of a reference photograph and a rendered capture, scaled to one height and labelled. `--reference`, `--render`, `--out`. | +| [`review/e2e_asset_check.py`](review/e2e_asset_check.py) | End-to-end check in one command: Blender renders → both Unreal maps rebuilt → captures for **both** the game map and the staging map → two-model judge → `Screenshots/README.md` regenerated. `--skip-blender`, `--skip-judge`, `--skip-recreate`. Report in `/tmp/gigline/e2e-report.txt`; non-zero exit if any step failed. | +| [`unreal/ue_recreate_maps.py`](unreal/ue_recreate_maps.py) | Deletes `DemoMap` and `ReviewMap` and creates both as empty levels, so a run never inherits an actor from an experiment. Chmods the LFS-read-only `.umap` first and parks the editor on a scratch level when it has the map open. | +| [`unreal/ue_clear_simple_collision.py`](unreal/ue_clear_simple_collision.py) | Removes the FBX import's auto-generated convex hulls from the site and street meshes and sets them to complex-as-simple, so physics bodies are not pushed out of a 279 m hull. | +| [`unreal/ue_vehicle_probe_map.py`](unreal/ue_vehicle_probe_map.py) | Copies `ReviewMap` to `VehicleProbe`, optionally adds one demo mesh and the Chaos pawn, and saves it, to bisect which map content throws a physics body into the air. Set `GIGLINE_PROBE_ASSET` to stage one mesh; unset for the control. | +| [`unreal/ue_build_review_map.py`](unreal/ue_build_review_map.py) | Rebuilds `Test/ReviewMap`, stages the shot camera list, and stages the demo-map views | +| [`unreal/ue_create_input_assets.py`](unreal/ue_create_input_assets.py) | Creates `/Game/Input` actions and the mapping context headlessly | +| [`unreal/ue_character_anim_import.py`](unreal/ue_character_anim_import.py) | Imports the animated FBX into `/Game/Test/Character` and **verifies an AnimSequence exists** — an FBX exported without `bake_anim` imports a mesh and no clips, with no error. Writes `VERDICT: PASS/FAIL` to `/tmp/gigline/character-anim-import.txt`. | +| [`unreal/ue_character_import.py`](unreal/ue_character_import.py) | Skeletal import plus the material/texture chain Interchange skips | ## Visual review | Tool | Purpose | | --- | --- | | `render_screenshots.py` / `render_screenshots.sh` | Rebuilds the review map, runs the game, captures the shot list into `Screenshots/` | -| `judge_screenshot.py` | Vision-model pre-check: judges a screenshot against an expected-scene description (`--image` + `--expect`, or `--shots` for every shot in the README). Reads `OPENROUTER_API_KEY` from `.env` (ignored). Judged by **two models by default** (`--models a,b`), reconciled: agreement carries the verdict, disagreement reports `uncertain` with both reasons. Optional, advisory only — never replaces a human verdict. | `--image` + `--expect`; `--model` for a single judgement | -| `visual_loop.py` | The reference-vs-render loop. `--verify` gates each capture against its expectation in `visual_targets.json` (blank/short/wrong-size/tone-less shots fail); `--compare` measures luminance, contrast, edge density, palette and 3×3 composition against the reference photograph and turns the deltas into modelling actions naming the skill to consult (`blender-modeler`, `archviz`, `materials`, `lookdev`, `camera-cinematography`); `--iteration N` writes the artefacts under `Art/reference/iterations/N/`, which is **gitignored until the owner confirms a shot**. | -| `visual_targets.json` | Per-shot expectation and reference photograph for `visual_loop.py`. Every entry must be a demo-map shot. | +| [`review/judge_screenshot.py`](review/judge_screenshot.py) | Vision-model pre-check: judges a screenshot against an expected-scene description (`--image` + `--expect`, or `--shots` for every shot in the README). Reads `OPENROUTER_API_KEY` from `.env` (ignored). Judged by **two models by default** (`--models a,b`), reconciled: agreement carries the verdict, disagreement reports `uncertain` with both reasons. Optional, advisory only — never replaces a human verdict. | `--image` + `--expect`; `--model` for a single judgement | +| [`review/visual_loop.py`](review/visual_loop.py) | The reference-vs-render loop. `--verify` gates each capture against its expectation in `visual_targets.json` (blank/short/wrong-size/tone-less shots fail); `--compare` measures luminance, contrast, edge density, palette and 3×3 composition against the reference photograph and turns the deltas into modelling actions naming the skill to consult (`blender-modeler`, `archviz`, `materials`, `lookdev`, `camera-cinematography`); `--iteration N` writes the artefacts under `Art/reference/iterations/N/`, which is **gitignored until the owner confirms a shot**. | +| [`review/visual_targets.json`](review/visual_targets.json) | Per-shot expectation and reference photograph for `visual_loop.py`. Every entry must be a demo-map shot. | ## Gameplay QA @@ -85,11 +102,11 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b | Tool | Purpose | | --- | --- | -| `comfyui_generate.py` | Drives the **local** ComfyUI at `http://127.0.0.1:8188` (Stability Matrix install). `models` lists checkpoints; `txt2img` renders through the HTTP API; **`texture` renders material sheets with the project's documented texture workflows** (see below). Every run writes a provenance sidecar (model, prompt, seed, settings, full graph). Two candidates by default — same prompt and seed, both scored by high-frequency detail, the winner takes `--out`, the loser is kept beside it. Local and human-triggered; no credential. | -| `generate_facade_textures.py` | Procedural, deterministic facade texture maps (ashlar base colour + roughness, stucco, asphalt, concrete) written with a pure-zlib PNG encoder — no model, no network, byte-identical on every run. | -| `generate_character_textures.py` | Procedural character texture maps, same deterministic approach as the facade set. | -| `facade_detail_concepts.py` | Generates **frontal building plates** (whole elevation, entrance in its facade, or building with neighbours) from a local reference photograph with an image model (OpenRouter); close-up detail crops are deliberately not requested because they cannot be measured for proportion. `--list` is free and shows which studies have their reference present; `--all`/`--only ` generates. Never overwrites (writes `-v2`, `-v3`), writes a `.refgen.json` sidecar per image (model, prompt, source photo, SHA-256), and puts everything under `Art/reference/facade-details/` — gitignored, local-only, never shipped. Costs money per image, so it is never automatic. | -| `judge_screenshot.py` | (see Visual review) | +| [`gen/comfyui_generate.py`](gen/comfyui_generate.py) | Drives the **local** ComfyUI at `http://127.0.0.1:8188` (Stability Matrix install). `models` lists checkpoints; `txt2img` renders through the HTTP API; **`texture` renders material sheets with the project's documented texture workflows** (see below). Every run writes a provenance sidecar (model, prompt, seed, settings, full graph). Two candidates by default — same prompt and seed, both scored by high-frequency detail, the winner takes `--out`, the loser is kept beside it. Local and human-triggered; no credential. | +| [`gen/generate_facade_textures.py`](gen/generate_facade_textures.py) | Procedural, deterministic facade texture maps (ashlar base colour + roughness, stucco, asphalt, concrete) written with a pure-zlib PNG encoder — no model, no network, byte-identical on every run. | +| [`gen/generate_character_textures.py`](gen/generate_character_textures.py) | Procedural character texture maps, same deterministic approach as the facade set. | +| [`gen/facade_detail_concepts.py`](gen/facade_detail_concepts.py) | Generates **frontal building plates** (whole elevation, entrance in its facade, or building with neighbours) from a local reference photograph with an image model (OpenRouter); close-up detail crops are deliberately not requested because they cannot be measured for proportion. `--list` is free and shows which studies have their reference present; `--all`/`--only ` generates. Never overwrites (writes `-v2`, `-v3`), writes a `.refgen.json` sidecar per image (model, prompt, source photo, SHA-256), and puts everything under `Art/reference/facade-details/` — gitignored, local-only, never shipped. Costs money per image, so it is never automatic. | +| [`review/judge_screenshot.py`](review/judge_screenshot.py) | (see Visual review) | ### Texture generation workflows (from the Stability Matrix install) @@ -102,7 +119,7 @@ The install documents four verified, core-node graphs in [`Workflows/texture_gen | `qwen21` | `qwen_image_2.1_int8_convrot.safetensors` | 25 / 1.0 / `euler` `simple` | texture-fix (Qwen 2.1) | material sheets with readable labels/signage, 2K | **Qwen research — non-commercial** | ```bash -python3 Tools/comfyui_generate.py texture \ +python3 Tools/gen/comfyui_generate.py texture \ --material "weathered grey limestone ashlar, fine grain" \ --out Art/textures/generated/T_StoneAshlar_AI.png ``` @@ -119,7 +136,7 @@ The default pair is `albedobase` + `krea2` — both commercially usable — gene `comfyui_generate.py` talks to ComfyUI on `http://127.0.0.1:8188`. That server is **not** started by this repository — it lives in a [Stability Matrix](https://lykos.ai) install and must already be running. -**If the tool reports a connection error, start Stability Matrix and launch ComfyUI**, then re-run. The install is documented in [`/home/zwitschi/StabilityMatrix/Data/README.md`](/home/zwitschi/StabilityMatrix/Data/README.md): package version, launch arguments, and the shared model library (`Models/`, ~80 GB, mapped in through `extra_model_paths.yaml`). Reading that file before writing a prompt is the cheap way to see which checkpoints actually exist — `python3 Tools/comfyui_generate.py models` asks the running server the same question, and it is the authoritative list — the default pair is `juggernautXL_ragnarok.safetensors` and `epicrealismXL_pureFix.safetensors`, both verified installed. +**If the tool reports a connection error, start Stability Matrix and launch ComfyUI**, then re-run. The install is documented in [`/home/zwitschi/StabilityMatrix/Data/README.md`](/home/zwitschi/StabilityMatrix/Data/README.md): package version, launch arguments, and the shared model library (`Models/`, ~80 GB, mapped in through `extra_model_paths.yaml`). Reading that file before writing a prompt is the cheap way to see which checkpoints actually exist — `python3 Tools/gen/comfyui_generate.py models` asks the running server the same question, and it is the authoritative list — the default pair is `juggernautXL_ragnarok.safetensors` and `epicrealismXL_pureFix.safetensors`, both verified installed. Checkpoints are **not** committed. A generated texture ships only after its model and licence are recorded in [`THIRD_PARTY_NOTICES.md`](../THIRD_PARTY_NOTICES.md); the sidecar carries what that needs. @@ -127,18 +144,18 @@ Checkpoints are **not** committed. A generated texture ships only after its mode | Tool | Purpose | | --- | --- | -| `input_emulation.py` | Pure-stdlib `ctypes`/`fcntl` driver that creates a virtual keyboard+mouse on `/dev/uinput`. The desktop treats it as real hardware, so it proves whether OS input actually reaches the game. Needs `/dev/uinput` writable (ACL on this host). | -| `input_test.py` | Injects a scripted sequence into the focused window: `--sequence move:2,look:3,jump:1,reset:1,pause:1,click`. | -| `comfyui_generate.py` | Local ComfyUI driver: list checkpoints, run txt2img through the HTTP API, and write a provenance sidecar (model, prompt, seed, full graph). **Two checkpoints by default**: both are generated from the same prompt and seed, scored by high-frequency detail, and the better one takes `--out` while the loser is kept beside it. Local, human-triggered. | `models`; `txt2img --prompt ... --out [--alt-model ...]` | `python3 Tools/comfyui_generate.py models` | -| `comfyui_character_blueprint.py` | Generates a character blueprint sheet - front, side, back and head views plus a contact sheet - from one description, with fixed seeds and a JSON record. | `--desc`, `--out`, `--model`, `--size` | `python3 Tools/comfyui_character_blueprint.py --desc "lean pedestrian, navy tee, jeans" --out Art/characters/blueprints` | -| `character_match.py` | Compares a rendered character against a blueprint: figure aspect, fill and coverage deltas, mean CIE76 colour distance, dominant-palette distance, and a side-by-side montage. | `--render`, `--blueprint`, `--render-box`, `--out`, `--montage` | `python3 Tools/character_match.py --render Screenshots/demo/review-demo-third-person-front.png --blueprint Art/characters/blueprints/front.png --render-box 0.42,0.46,0.56,0.97 --montage /tmp/gigline/match.png` | -| `generate_character_textures.py` | Procedural skin-pore and hair-strand textures with sidecars; no model involved. | `--out` | `python3 Tools/generate_character_textures.py --out Art/textures/generated` | -| `vehicle_build.py` | Models the drivable sedan: three-tier body, four wheels, lamps and bumpers, plus four `UCX_` convex collision hulls; saves the .blend, exports an FBX with the `ue_metric_v1` preset, and writes a report with every dimension the vehicle config needs. | `--out`, `--fbx`, `--report` | `blender -b --factory-startup --python Tools/vehicle_build.py -- --out Art/blender/vehicle/gigline-sedan.blend --fbx /tmp/gigline/vehicle/sedan.fbx` | -| `ue_vehicle_import.py` | Imports the sedan's six systems into `/Game/Hollywood/Vehicle` as separate static meshes, plus the two LOD meshes; the body carries the `UCX_` hulls as simple collision. | (no arguments; reads `/tmp/gigline/vehicle`) | `UnrealEditor-Cmd -run=pythonscript -script=Tools/ue_vehicle_import.py -nullrhi -unattended -nopause` | -| `vehicle_from_template.py` | Converts the owner's `hot hatch.blend` into UE-ready assets: normalises the scale to a documented 4.05 m, splits systems (body/trim/glass/lights/wheels), adds a `UCX_` hull, decimates two LODs, exports FBX per system and writes a report. | `--out`, `--report` | `blender -b "Art/blender/vehicle/hot hatch.blend" --python Tools/vehicle_from_template.py -- --out /tmp/gigline/hatch` | -| `blender_screenshots.py` | Renders review screenshots out of a .blend: one per mesh object (`--mode objects`) and four turntable angles (`--mode scene`), framed from the scene's own bounds, into `Screenshots/blender/`. | `--label`, `--mode`, `--out`, `--report` | `blender -b "Art/blender/vehicle/hot hatch.blend" --python Tools/blender_screenshots.py -- --label vehicle --mode both` | -| `build_comparison_md.py` | Regenerates the `Blender vs in-game` section of `Screenshots/README.md` between `` markers: in-game captures beside their Blender renders in one table, plus a signoff table with one row per in-game screen. Pairs by name with an alias map for camera-named shots. Run it after every capture or Blender render. | (no arguments) | `python3 Tools/build_comparison_md.py` | -| `shadow_gap.py` | Measures the vertical pixel gap between an object's lowest pixels and the top of its shadow, as a fraction of the object's height, and calls it `contact` or `floating`. Objective replacement for asking a vision model whether something floats. | `--image`, `--object-box`, `--gap-fraction`, `--out` | `python3 Tools/shadow_gap.py --image Screenshots/demo/review-demo-spawn-view.png --object-box 0.44,0.48,0.60,0.99` | +| [`input/input_emulation.py`](input/input_emulation.py) | Pure-stdlib `ctypes`/`fcntl` driver that creates a virtual keyboard+mouse on `/dev/uinput`. The desktop treats it as real hardware, so it proves whether OS input actually reaches the game. Needs `/dev/uinput` writable (ACL on this host). | +| [`input/input_test.py`](input/input_test.py) | Injects a scripted sequence into the focused window: `--sequence move:2,look:3,jump:1,reset:1,pause:1,click`. | +| [`gen/comfyui_generate.py`](gen/comfyui_generate.py) | Local ComfyUI driver: list checkpoints, run txt2img through the HTTP API, and write a provenance sidecar (model, prompt, seed, full graph). **Two checkpoints by default**: both are generated from the same prompt and seed, scored by high-frequency detail, and the better one takes `--out` while the loser is kept beside it. Local, human-triggered. | `models`; `txt2img --prompt ... --out [--alt-model ...]` | `python3 Tools/gen/comfyui_generate.py models` | +| [`blender/comfyui_character_blueprint.py`](blender/comfyui_character_blueprint.py) | Generates a character blueprint sheet - front, side, back and head views plus a contact sheet - from one description, with fixed seeds and a JSON record. | `--desc`, `--out`, `--model`, `--size` | `python3 Tools/comfyui_character_blueprint.py --desc "lean pedestrian, navy tee, jeans" --out Art/characters/blueprints` | +| [`blender/character_match.py`](blender/character_match.py) | Compares a rendered character against a blueprint: figure aspect, fill and coverage deltas, mean CIE76 colour distance, dominant-palette distance, and a side-by-side montage. | `--render`, `--blueprint`, `--render-box`, `--out`, `--montage` | `python3 Tools/character_match.py --render Screenshots/demo/review-demo-third-person-front.png --blueprint Art/characters/blueprints/front.png --render-box 0.42,0.46,0.56,0.97 --montage /tmp/gigline/match.png` | +| [`gen/generate_character_textures.py`](gen/generate_character_textures.py) | Procedural skin-pore and hair-strand textures with sidecars; no model involved. | `--out` | `python3 Tools/generate_character_textures.py --out Art/textures/generated` | +| [`blender/vehicle_build.py`](blender/vehicle_build.py) | Models the drivable sedan: three-tier body, four wheels, lamps and bumpers, plus four `UCX_` convex collision hulls; saves the .blend, exports an FBX with the `ue_metric_v1` preset, and writes a report with every dimension the vehicle config needs. | `--out`, `--fbx`, `--report` | `blender -b --factory-startup --python Tools/vehicle_build.py -- --out Art/blender/vehicle/gigline-sedan.blend --fbx /tmp/gigline/vehicle/sedan.fbx` | +| [`unreal/ue_vehicle_import.py`](unreal/ue_vehicle_import.py) | Imports the sedan's six systems into `/Game/Hollywood/Vehicle` as separate static meshes, plus the two LOD meshes; the body carries the `UCX_` hulls as simple collision. | (no arguments; reads `/tmp/gigline/vehicle`) | `UnrealEditor-Cmd -run=pythonscript -script=Tools/ue_vehicle_import.py -nullrhi -unattended -nopause` | +| [`blender/vehicle_from_template.py`](blender/vehicle_from_template.py) | Converts the owner's `hot hatch.blend` into UE-ready assets: normalises the scale to a documented 4.05 m, splits systems (body/trim/glass/lights/wheels), adds a `UCX_` hull, decimates two LODs, exports FBX per system and writes a report. | `--out`, `--report` | `blender -b "Art/blender/vehicle/hot hatch.blend" --python Tools/vehicle_from_template.py -- --out /tmp/gigline/hatch` | +| [`blender/blender_screenshots.py`](blender/blender_screenshots.py) | Renders review screenshots out of a .blend: one per mesh object (`--mode objects`) and four turntable angles (`--mode scene`), framed from the scene's own bounds, into `Screenshots/blender/`. | `--label`, `--mode`, `--out`, `--report` | `blender -b "Art/blender/vehicle/hot hatch.blend" --python Tools/blender/blender_screenshots.py -- --label vehicle --mode both` | +| [`review/build_comparison_md.py`](review/build_comparison_md.py) | Regenerates the `Blender vs in-game` section of `Screenshots/README.md` between `` markers: in-game captures beside their Blender renders in one table, plus a signoff table with one row per in-game screen. Pairs by name with an alias map for camera-named shots. Run it after every capture or Blender render. | (no arguments) | `python3 Tools/review/build_comparison_md.py` | +| [`review/shadow_gap.py`](review/shadow_gap.py) | Measures the vertical pixel gap between an object's lowest pixels and the top of its shadow, as a fraction of the object's height, and calls it `contact` or `floating`. Objective replacement for asking a vision model whether something floats. | `--image`, `--object-box`, `--gap-fraction`, `--out` | `python3 Tools/shadow_gap.py --image Screenshots/demo/review-demo-spawn-view.png --object-box 0.44,0.48,0.60,0.99` | ## Tests diff --git a/Tools/blender_screenshots.py b/Tools/blender/blender_screenshots.py similarity index 97% rename from Tools/blender_screenshots.py rename to Tools/blender/blender_screenshots.py index 1212d40..a69b69a 100644 --- a/Tools/blender_screenshots.py +++ b/Tools/blender/blender_screenshots.py @@ -2,7 +2,7 @@ """Render review screenshots straight out of Blender. blender -b "Art/blender/vehicle/hot hatch.blend" \ - --python Tools/blender_screenshots.py -- \ + --python Tools/blender/blender_screenshots.py -- \ --label vehicle --mode both --out Screenshots/blender Two modes, because two questions get asked about a Blender asset: @@ -14,7 +14,7 @@ Two modes, because two questions get asked about a Blender asset: proportion against the in-game shot. Outputs go beside the in-game captures (`Screenshots/blender/`), one folder layer -below, so `Tools/build_comparison_md.py` can pair them and nothing has to be copied by +below, so `Tools/review/build_comparison_md.py` can pair them and nothing has to be copied by hand. Every render is framed from the scene's own bounds rather than a hand-placed camera, so adding geometry does not silently crop the shot. """ diff --git a/Tools/character_anim_build.py b/Tools/blender/character_anim_build.py similarity index 99% rename from Tools/character_anim_build.py rename to Tools/blender/character_anim_build.py index 22a952d..b017c63 100644 --- a/Tools/character_anim_build.py +++ b/Tools/blender/character_anim_build.py @@ -9,7 +9,7 @@ re-derived from the numbers below: GigLine_Walk 30 frames @ 30 fps — one full stride, left and right contact blender -b Art/blender/character/character-2026-11-07.blend \ - --python Tools/character_anim_build.py -- \ + --python Tools/blender/character_anim_build.py -- \ --out /tmp/gigline/character-anim.fbx \ --report /tmp/gigline/character-anim-report.json @@ -38,7 +38,7 @@ import sys import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset as fbx_preset # noqa: E402 FPS = 30 diff --git a/Tools/character_clothing_build.py b/Tools/blender/character_clothing_build.py similarity index 98% rename from Tools/character_clothing_build.py rename to Tools/blender/character_clothing_build.py index 61edaa3..20527c7 100644 --- a/Tools/character_clothing_build.py +++ b/Tools/blender/character_clothing_build.py @@ -6,7 +6,7 @@ faces for the clothing base, solidify for cloth thickness, and keep each piece a separate mesh so it can be swapped or re-materialled on its own. blender -b Art/blender/character/character-2026-11-07.blend \ - --python Tools/character_clothing_build.py -- \ + --python Tools/blender/character_clothing_build.py -- \ --out /tmp/gigline/clothing --report /tmp/gigline/clothing.json Outputs one FBX per garment (`tee`, `jeans`, `shoes`), each carrying the character @@ -26,7 +26,7 @@ import sys import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset as fbx_preset # noqa: E402 GARMENTS = ( diff --git a/Tools/character_hair_build.py b/Tools/blender/character_hair_build.py similarity index 97% rename from Tools/character_hair_build.py rename to Tools/blender/character_hair_build.py index fec6c9b..efb112d 100644 --- a/Tools/character_hair_build.py +++ b/Tools/blender/character_hair_build.py @@ -8,13 +8,13 @@ slot on a skeletal-mesh import, so the cap rendered with the skin material. A se object sidesteps that and is what the skill calls for anyway. blender -b Art/blender/character/character-2026-11-07.blend \ - --python Tools/character_hair_build.py -- \ + --python Tools/blender/character_hair_build.py -- \ --out /tmp/gigline/character-hair.fbx --report /tmp/gigline/character-hair.json What it builds: SM_Char_Hair scalp cap + 18 cards in three rings, UV-mapped to the strand sheet - from Tools/generate_character_textures.py, every vertex weighted + from Tools/gen/generate_character_textures.py, every vertex weighted to `head` at 1.0 so it follows the head without a second rig. Cards are built with a taper and a slight outward curl so the silhouette is not a @@ -35,7 +35,7 @@ import sys import bpy from mathutils import Vector -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset as fbx_preset # noqa: E402 HAIR_NAME = "SM_Char_Hair" diff --git a/Tools/character_match.py b/Tools/blender/character_match.py similarity index 99% rename from Tools/character_match.py rename to Tools/blender/character_match.py index 85a96b3..07214af 100644 --- a/Tools/character_match.py +++ b/Tools/blender/character_match.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 """Evaluate a rendered character against a generated blueprint. -The blueprint comes from `Tools/comfyui_character_blueprint.py`; the render comes from +The blueprint comes from `Tools/blender/comfyui_character_blueprint.py`; the render comes from the review pipeline. This tool isolates the figure in both, then reports how far the model has drifted from the intended character: - python3 Tools/character_match.py \ + python3 Tools/blender/character_match.py \ --render Screenshots/demo/review-demo-third-person-front.png \ --blueprint Art/characters/blueprints/front.png \ --out /tmp/gigline/character-match.json \ diff --git a/Tools/comfyui_character_blueprint.py b/Tools/blender/comfyui_character_blueprint.py similarity index 92% rename from Tools/comfyui_character_blueprint.py rename to Tools/blender/comfyui_character_blueprint.py index 905e6ca..3a9311c 100644 --- a/Tools/comfyui_character_blueprint.py +++ b/Tools/blender/comfyui_character_blueprint.py @@ -4,16 +4,16 @@ A "blueprint" here is an art-direction sheet for one character: a front, side and back view, plus a head detail, generated from a fixed prompt and seed set so the sheet can be regenerated and compared against later. It is the reference the modelled character -is evaluated against — see `Tools/character_match.py`. +is evaluated against — see `Tools/blender/character_match.py`. - python3 Tools/comfyui_character_blueprint.py \ + python3 Tools/blender/comfyui_character_blueprint.py \ --character "GigLine walker" \ --desc "lean adult pedestrian, short dark hair, olive skin, plain navy t-shirt, dark jeans, worn sneakers" \ --out Art/characters/blueprints Writes one PNG per view plus `contact-sheet.png` and `blueprint.json`, which records the prompt, model, seeds and the ComfyUI prompt ids. Everything is generated locally -by `Tools/comfyui_generate.py`; no network service and no credential is involved. +by `Tools/gen/comfyui_generate.py`; no network service and no credential is involved. The sheet is generated art, not photography, and it stays out of the reference photo folders on purpose: AGENTS.md forbids committing reference photography, and a @@ -27,7 +27,7 @@ import json import os import sys -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import comfyui_generate as comfy # noqa: E402 from PIL import Image # noqa: E402 @@ -106,7 +106,7 @@ def main(argv=None): sheet = contact_sheet(written, os.path.join(args.out, "contact-sheet.png")) record["contact_sheet"] = sheet - record["note"] = ("generated locally by Tools/comfyui_character_blueprint.py; " + record["note"] = ("generated locally by Tools/blender/comfyui_character_blueprint.py; " "record the model licence in THIRD_PARTY_NOTICES.md before shipping") with open(os.path.join(args.out, "blueprint.json"), "w") as handle: json.dump(record, handle, indent=2) diff --git a/Tools/export_block_frontage.py b/Tools/blender/export_block_frontage.py similarity index 95% rename from Tools/export_block_frontage.py rename to Tools/blender/export_block_frontage.py index a03d9df..3e41d0a 100644 --- a/Tools/export_block_frontage.py +++ b/Tools/blender/export_block_frontage.py @@ -1,7 +1,7 @@ # Export the south-side block frontage from the OSM blockout. # # blender -b --factory-startup Art/blender/blockout/blockout-2026-11-06.blend \ -# --python Tools/export_block_frontage.py -- --out /tmp/gigline/frontage +# --python Tools/blender/export_block_frontage.py -- --out /tmp/gigline/frontage # # The building set is the one verified in # `Geo/verification/2026-11-06-el-capitan-block.md` ("South frontage, west to @@ -19,7 +19,7 @@ import os import sys import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset as fbx_preset # noqa: E402 diff --git a/Tools/export_north_buildings.py b/Tools/blender/export_north_buildings.py similarity index 93% rename from Tools/export_north_buildings.py rename to Tools/blender/export_north_buildings.py index 06dfd50..a63999c 100644 --- a/Tools/export_north_buildings.py +++ b/Tools/blender/export_north_buildings.py @@ -1,7 +1,7 @@ # Export the two in-scope north-side background buildings from the OSM blockout. # # blender -b --factory-startup Art/blender/blockout/blockout-2026-11-06.blend \ -# --python Tools/export_north_buildings.py -- --out /tmp/gigline/north +# --python Tools/blender/export_north_buildings.py -- --out /tmp/gigline/north # # The blockout is already in the shared project frame (metres, X east / Y north / # Z up, origin Hollywood x Highland), so exporting each object at its own world @@ -13,7 +13,7 @@ import os import sys import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset as fbx_preset # noqa: E402 diff --git a/Tools/facade_kit_build.py b/Tools/blender/facade_kit_build.py similarity index 97% rename from Tools/facade_kit_build.py rename to Tools/blender/facade_kit_build.py index 30b8ff0..66d19b8 100644 --- a/Tools/facade_kit_build.py +++ b/Tools/blender/facade_kit_build.py @@ -23,9 +23,9 @@ where it resolves them (window proportions, shopfront line); everything the photographs do not resolve is an assumption and is recorded as one in the part report, so model and evidence stay separable. - python3 Tools/facade_kit_build.py --out /tmp/gigline/kit \ + python3 Tools/blender/facade_kit_build.py --out /tmp/gigline/kit \ --report /tmp/gigline/kit-report.json - blender -b --factory-startup --python Tools/facade_kit_build.py -- --out /tmp/gigline/kit + blender -b --factory-startup --python Tools/blender/facade_kit_build.py -- --out /tmp/gigline/kit """ from __future__ import annotations @@ -38,7 +38,7 @@ import sys import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset as fbx_preset # noqa: E402 # --- module ------------------------------------------------------------------ diff --git a/Tools/fbx_export_preset.py b/Tools/blender/fbx_export_preset.py similarity index 100% rename from Tools/fbx_export_preset.py rename to Tools/blender/fbx_export_preset.py diff --git a/Tools/hero_facade_build.py b/Tools/blender/hero_facade_build.py similarity index 99% rename from Tools/hero_facade_build.py rename to Tools/blender/hero_facade_build.py index 5780031..99a9247 100644 --- a/Tools/hero_facade_build.py +++ b/Tools/blender/hero_facade_build.py @@ -4,7 +4,7 @@ Builds the north elevation in the project frame (X east, Y north, Z up, metres, origin Hollywood x Highland) and exports it with the named FBX preset `ue_metric_v1`, so it imports into Unreal at 1 m = 100 uu. - blender -b --factory-startup --python Tools/hero_facade_build.py -- \ + blender -b --factory-startup --python Tools/blender/hero_facade_build.py -- \ --out /tmp/gigline/hero-facade.fbx \ --save-blend Art/blender/hero/hero-facade-2026-11-07.blend \ --report Art/blender/hero/hero-facade-2026-11-07.json @@ -33,7 +33,7 @@ import os import sys import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset as fbx_preset # noqa: E402 @@ -463,9 +463,9 @@ def seat_on_front(table): def export_fbx(path): - # Preset lives in Tools/fbx_export_preset.py (ue_metric_v1). + # Preset lives in Tools/blender/fbx_export_preset.py (ue_metric_v1). os.makedirs(os.path.dirname(path), exist_ok=True) - # Named export preset "ue_metric_v1" - same settings as Tools/scale_test_export.py. + # Named export preset "ue_metric_v1" - same settings as Tools/blender/scale_test_export.py. fbx_preset.export_static(path) print(f"[hero] exported FBX preset ue_metric_v1 -> {path}") diff --git a/Tools/mpfb_character_export.py b/Tools/blender/mpfb_character_export.py similarity index 97% rename from Tools/mpfb_character_export.py rename to Tools/blender/mpfb_character_export.py index a37dd4a..3cfc0b7 100644 --- a/Tools/mpfb_character_export.py +++ b/Tools/blender/mpfb_character_export.py @@ -4,7 +4,7 @@ # materializes the v2 skin, bakes its diffuse to a texture, and exports # FBX with the named export preset "ue_character_v1". # -# blender -b --factory-startup --python Tools/mpfb_character_export.py -- \ +# blender -b --factory-startup --python Tools/blender/mpfb_character_export.py -- \ # --out /tmp/gigline/character.fbx --pose idle \ # --save-blend Art/blender/character/character-.blend @@ -19,7 +19,7 @@ import sys import zlib import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset as fbx_preset # noqa: E402 import numpy diff --git a/Tools/retail_frontage_build.py b/Tools/blender/retail_frontage_build.py similarity index 98% rename from Tools/retail_frontage_build.py rename to Tools/blender/retail_frontage_build.py index 5736dcd..d26b1b1 100644 --- a/Tools/retail_frontage_build.py +++ b/Tools/blender/retail_frontage_build.py @@ -8,9 +8,9 @@ stepped Deco parapet on Lee Drug, and the pilaster-and-fascia rhythm on 6806–6 The trim is built **in world coordinates** in the shared frame (metres, X east, Y north, Z from the project datum), so the export carries its position and the Unreal staging step is a single spawn at the origin — the same convention as -`Tools/theatre_trim_build.py` and `Tools/export_block_frontage.py`. +`Tools/blender/theatre_trim_build.py` and `Tools/blender/export_block_frontage.py`. - blender -b --factory-startup --python Tools/retail_frontage_build.py -- \ + blender -b --factory-startup --python Tools/blender/retail_frontage_build.py -- \ --out /tmp/gigline/retail --report /tmp/gigline/retail-report.json Bounds are **measured**, not assumed: they are the blockout objects' world bounds @@ -32,7 +32,7 @@ import sys import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import _common # noqa: E402 import fbx_export_preset as fbx_preset # noqa: E402 diff --git a/Tools/scale_test_export.py b/Tools/blender/scale_test_export.py similarity index 93% rename from Tools/scale_test_export.py rename to Tools/blender/scale_test_export.py index ffac49e..865a5cc 100644 --- a/Tools/scale_test_export.py +++ b/Tools/blender/scale_test_export.py @@ -8,7 +8,7 @@ # and exports them as one FBX with the named export preset "ue_metric_v1". # # Run headless (see Art/blender/README.md for the Flatpak invocation): -# blender -b --factory-startup --python Tools/scale_test_export.py -- \ +# blender -b --factory-startup --python Tools/blender/scale_test_export.py -- \ # --out /tmp/gigline/scaletest.fbx --save-blend Art/blender/scaletest/scale-test-.blend import argparse @@ -16,7 +16,7 @@ import os import sys import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset as fbx_preset # noqa: E402 diff --git a/Tools/theatre_trim_build.py b/Tools/blender/theatre_trim_build.py similarity index 97% rename from Tools/theatre_trim_build.py rename to Tools/blender/theatre_trim_build.py index 309bdfc..539ba9d 100644 --- a/Tools/theatre_trim_build.py +++ b/Tools/blender/theatre_trim_build.py @@ -10,7 +10,7 @@ convention as the block frontage and the north buildings. The building's verified span (from `Geo/verification/2026-11-06-el-capitan-block.md` and the staged mesh) is X −12082..−8210, street face at Y ≈ +1001, height 20.55 m. - blender -b --factory-startup --python Tools/theatre_trim_build.py -- \ + blender -b --factory-startup --python Tools/blender/theatre_trim_build.py -- \ --out /tmp/gigline/theatre --report /tmp/gigline/theatre-report.json Everything the photographs do not resolve at this scale (exact marquee depth, @@ -27,7 +27,7 @@ import sys import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset as fbx_preset # noqa: E402 # Verified building span, metres (Unreal X, Y, Z but Blender-frame: X east, diff --git a/Tools/vehicle_build.py b/Tools/blender/vehicle_build.py similarity index 99% rename from Tools/vehicle_build.py rename to Tools/blender/vehicle_build.py index 59cef6b..e106135 100644 --- a/Tools/vehicle_build.py +++ b/Tools/blender/vehicle_build.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Model a drivable sedan in Blender: body, wheels, collision and LOD. - blender -b --factory-startup --python Tools/vehicle_build.py -- \ + blender -b --factory-startup --python Tools/blender/vehicle_build.py -- \ --out Art/blender/vehicle/gigline-sedan.blend \ --fbx /tmp/gigline/vehicle/sedan.fbx \ --report /tmp/gigline/vehicle/report.json @@ -31,7 +31,7 @@ import sys import bpy from mathutils import Vector -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset # noqa: E402 # Real sedan reference: length, width, height, wheelbase, track, wheel radius. diff --git a/Tools/vehicle_from_template.py b/Tools/blender/vehicle_from_template.py similarity index 99% rename from Tools/vehicle_from_template.py rename to Tools/blender/vehicle_from_template.py index cf4f7b4..635382d 100644 --- a/Tools/vehicle_from_template.py +++ b/Tools/blender/vehicle_from_template.py @@ -2,7 +2,7 @@ """Turn the provided hot-hatch .blend into UE-ready assets. blender -b "Art/blender/vehicle/hot hatch.blend" \ - --python Tools/vehicle_from_template.py -- \ + --python Tools/blender/vehicle_from_template.py -- \ --out /tmp/gigline/hatch --report /tmp/gigline/hatch/report.json Follows two skills: @@ -28,7 +28,7 @@ import sys import bpy -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import fbx_export_preset # noqa: E402 # A three-door hot hatch: 4.05 m long, 1.75 m wide, 1.42 m tall. diff --git a/Tools/comfyui_generate.py b/Tools/gen/comfyui_generate.py similarity index 98% rename from Tools/comfyui_generate.py rename to Tools/gen/comfyui_generate.py index 9be0d5b..2704677 100644 --- a/Tools/comfyui_generate.py +++ b/Tools/gen/comfyui_generate.py @@ -7,10 +7,10 @@ and surface grime. ComfyUI runs locally at `127.0.0.1:8188`, so this is a no credential is involved. # what is installed - python3 Tools/comfyui_generate.py models + python3 Tools/gen/comfyui_generate.py models # one image - python3 Tools/comfyui_generate.py txt2img \ + python3 Tools/gen/comfyui_generate.py txt2img \ --prompt "seamless photoreal human skin texture, fine pores, neutral light" \ --negative "text, watermark, hair, clothing" \ --model juggernautXL_ragnarok.safetensors \ @@ -236,7 +236,7 @@ def run_graph(graph, out_path, meta) -> int: meta = dict(meta) meta.pop("timeout", None) meta.pop("poll", None) - meta.update({"tool": "Tools/comfyui_generate.py", + meta.update({"tool": "Tools/gen/comfyui_generate.py", "generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "workflow": graph, "comfyui_prompt_id": prompt_id, "note": "generated locally; record the model licence in " @@ -304,7 +304,7 @@ def generate(args) -> int: sidecar = os.path.splitext(args.out)[0] + ".json" with open(sidecar, "w") as handle: json.dump({ - "tool": "Tools/comfyui_generate.py", + "tool": "Tools/gen/comfyui_generate.py", "generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "prompt": args.prompt, "negative": args.negative, diff --git a/Tools/commons_fetch.py b/Tools/gen/commons_fetch.py similarity index 96% rename from Tools/commons_fetch.py rename to Tools/gen/commons_fetch.py index 38845b4..55d6f3e 100755 --- a/Tools/commons_fetch.py +++ b/Tools/gen/commons_fetch.py @@ -7,13 +7,13 @@ committed: the output directory is gitignored. The tool records provenance working copy stays traceable. # dry run: list what the search returns, download nothing - python3 Tools/commons_fetch.py --query "Grauman's Chinese Theatre" --slug north-chinese --dry-run + python3 Tools/gen/commons_fetch.py --query "Grauman's Chinese Theatre" --slug north-chinese --dry-run # download up to 6 images at 1600 px wide - python3 Tools/commons_fetch.py --query "Grauman's Chinese Theatre" --slug north-chinese --limit 6 + python3 Tools/gen/commons_fetch.py --query "Grauman's Chinese Theatre" --slug north-chinese --limit 6 # fetch specific files by title - python3 Tools/commons_fetch.py --file "File:Grauman's Chinese Theatre, Hollywood.JPG" --slug north-chinese + python3 Tools/gen/commons_fetch.py --file "File:Grauman's Chinese Theatre, Hollywood.JPG" --slug north-chinese Defaults: out dir `Art/reference//`, 1600 px wide thumbnails, one request per second, JPEG/PNG only, at least 800 px wide on the original. @@ -245,7 +245,7 @@ def main() -> int: "requested_files": args.file, "slug": args.slug, "thumb_width": args.width, - "tool": "Tools/commons_fetch.py", + "tool": "Tools/gen/commons_fetch.py", "note": "local development reference only; not committed to the repository", "images": records, } diff --git a/Tools/facade_detail_concepts.py b/Tools/gen/facade_detail_concepts.py similarity index 98% rename from Tools/facade_detail_concepts.py rename to Tools/gen/facade_detail_concepts.py index 90a523f..93fcc99 100644 --- a/Tools/facade_detail_concepts.py +++ b/Tools/gen/facade_detail_concepts.py @@ -10,10 +10,10 @@ pass has something measurable instead of a tourist photo. Deliberately **not** close-up detail crops: a doorway or cornice crop cannot be measured for proportion, which is what the modelling pass actually needs. - python3 Tools/facade_detail_concepts.py --list - python3 Tools/facade_detail_concepts.py --only door-recess,parapet-motto - python3 Tools/facade_detail_concepts.py --all - python3 Tools/facade_detail_concepts.py --all --model google/gemini-2.5-flash-image + python3 Tools/gen/facade_detail_concepts.py --list + python3 Tools/gen/facade_detail_concepts.py --only door-recess,parapet-motto + python3 Tools/gen/facade_detail_concepts.py --all + python3 Tools/gen/facade_detail_concepts.py --all --model google/gemini-2.5-flash-image Output: `Art/reference/facade-details/.png` plus a `.refgen.json` sidecar recording the model, the source photograph, the prompt and the SHA-256 of @@ -45,7 +45,7 @@ import time import urllib.error import urllib.request -ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) REFERENCE_DIR = os.path.join(ROOT, "Art", "reference") OUT_DIR = os.path.join(REFERENCE_DIR, "facade-details") API_URL = "https://openrouter.ai/api/v1/chat/completions" diff --git a/Tools/generate_character_textures.py b/Tools/gen/generate_character_textures.py similarity index 97% rename from Tools/generate_character_textures.py rename to Tools/gen/generate_character_textures.py index a75ba13..659331b 100644 --- a/Tools/generate_character_textures.py +++ b/Tools/gen/generate_character_textures.py @@ -11,7 +11,7 @@ So skin detail is generated here instead: fine tileable noise with a low-frequen break-up, which is what the base colour actually needs (pores, not a pattern). Hair is generated as a strand sheet with alpha, for hair cards. - python3 Tools/generate_character_textures.py --out Art/textures/generated + python3 Tools/gen/generate_character_textures.py --out Art/textures/generated """ import argparse @@ -154,7 +154,7 @@ def main(): path, meta = maker(spec, args.out) sidecar = os.path.splitext(path)[0] + ".json" with open(sidecar, "w") as handle: - json.dump({"tool": "Tools/generate_character_textures.py", + json.dump({"tool": "Tools/gen/generate_character_textures.py", "asset": spec["name"], "size": spec["size"], "procedural": True, "meta": meta, "note": "generated, not photographed; no third-party licence"}, diff --git a/Tools/generate_facade_textures.py b/Tools/gen/generate_facade_textures.py similarity index 97% rename from Tools/generate_facade_textures.py rename to Tools/gen/generate_facade_textures.py index f42488a..d1d3462 100644 --- a/Tools/generate_facade_textures.py +++ b/Tools/gen/generate_facade_textures.py @@ -9,9 +9,9 @@ each as base colour plus a roughness map so the specular breaks up. Standard library only — the PNG encoder is the same pure-zlib writer used for the MPFB skin, so this runs on the host without numpy or Pillow. - python3 Tools/generate_facade_textures.py # write to /tmp/gigline/textures - python3 Tools/generate_facade_textures.py --size 1024 - python3 Tools/generate_facade_textures.py --out Art/blender/textures + python3 Tools/gen/generate_facade_textures.py # write to /tmp/gigline/textures + python3 Tools/gen/generate_facade_textures.py --size 1024 + python3 Tools/gen/generate_facade_textures.py --out Art/blender/textures Output names match the Unreal import destination `/Game/Hollywood/Textures`: diff --git a/Tools/page_image_fetch.py b/Tools/gen/page_image_fetch.py similarity index 97% rename from Tools/page_image_fetch.py rename to Tools/gen/page_image_fetch.py index cad4ca9..238cc26 100644 --- a/Tools/page_image_fetch.py +++ b/Tools/gen/page_image_fetch.py @@ -6,7 +6,7 @@ an API: the page's `og:image` (or `twitter:image`) is the image the site itself publishes as representative, so it is a defensible pick without scraping every `` on the page. - python3 Tools/page_image_fetch.py \ + python3 Tools/gen/page_image_fetch.py \ --page "https://www.flickr.com/photos/…/in/…" --slug hero-masonic --name entry-01 Writes into `Art/reference//` (gitignored) and appends provenance to @@ -121,7 +121,7 @@ def main(): print(f" saved {filename} ({len(blob)//1024} KB)") sidecar_path = os.path.join(out_dir, "sidecar-web.json") - data = {"source": "web page og:image", "tool": "Tools/page_image_fetch.py", + data = {"source": "web page og:image", "tool": "Tools/gen/page_image_fetch.py", "note": "local development reference only; not committed", "images": []} if os.path.exists(sidecar_path): try: diff --git a/Tools/input_emulation.py b/Tools/input/input_emulation.py similarity index 99% rename from Tools/input_emulation.py rename to Tools/input/input_emulation.py index 39901b2..d9901f8 100644 --- a/Tools/input_emulation.py +++ b/Tools/input/input_emulation.py @@ -2,7 +2,7 @@ """Pure-Python uinput driver to inject OS-level keyboard and mouse input. This is a small, dependency-free input driver used by the demo QA harness -(`Tools/input_test.py`) to prove whether OS input actually reaches the game. +(`Tools/input/input_test.py`) to prove whether OS input actually reaches the game. It talks to ``/dev/uinput`` directly through ``ctypes``, so it creates a real virtual input device that the kernel and the desktop session (X11 or Wayland) treat as genuine hardware. The game cannot tell simulated input from a real diff --git a/Tools/input_test.py b/Tools/input/input_test.py similarity index 92% rename from Tools/input_test.py rename to Tools/input/input_test.py index c37bd3a..5c83481 100644 --- a/Tools/input_test.py +++ b/Tools/input/input_test.py @@ -2,7 +2,7 @@ """QA harness: inject scripted OS-level keyboard/mouse input at the running game. This is the tool that proves whether OS input actually reaches the game. It uses -`Tools/input_emulation.py` to create a virtual input device directly on +`Tools/input/input_emulation.py` to create a virtual input device directly on `/dev/uinput`, so a focused game window receives the injected keys and mouse movement as genuine hardware input — indistinguishable from a real keyboard. @@ -11,7 +11,7 @@ Typical workflow The game must already be running and focused (it will not be launched here; the user brings up the window). Run this script from a separate terminal: - python3 Tools/input_test.py --sequence move:2,look:3,jump:1,reset:1,pause:1 + python3 Tools/input/input_test.py --sequence move:2,look:3,jump:1,reset:1,pause:1 It prints a per-step timeline (seconds) to stdout, which you can compare against the `GigLine IN:` rows in the game's `Saved/Logs`. If the game log shows @@ -20,9 +20,9 @@ the pawn and the remaining problem is not input delivery. Usage ----- - python3 Tools/input_test.py # default smoke sequence - python3 Tools/input_test.py --sequence move:2,look:3,jump:1,reset:1,pause:1 - python3 Tools/input_test.py --device /dev/uinput --name "GigLine QA Input" + python3 Tools/input/input_test.py # default smoke sequence + python3 Tools/input/input_test.py --sequence move:2,look:3,jump:1,reset:1,pause:1 + python3 Tools/input/input_test.py --device /dev/uinput --name "GigLine QA Input" Steps supported in `--sequence` (comma-separated, `name:count`): move hold W forward (each unit = ~0.5 s) diff --git a/Tools/run_pie.sh b/Tools/input/run_pie.sh similarity index 94% rename from Tools/run_pie.sh rename to Tools/input/run_pie.sh index b6b4f94..b448237 100755 --- a/Tools/run_pie.sh +++ b/Tools/input/run_pie.sh @@ -2,7 +2,7 @@ # Open the Unreal editor focused on the walkable demo so you can press Play (PIE) # to compare in-editor input intake vs the packaged standalone build. # -# Usage: Tools/run_pie.sh +# Usage: Tools/input/run_pie.sh # Then in the editor: press the green Play button (Alt+P or the PIE toolbar). set -e ENGINE="${UE_ENGINE:-/run/media/system/DATA/Engines/UE_5.8.4}" diff --git a/Tools/build_comparison_md.py b/Tools/review/build_comparison_md.py similarity index 96% rename from Tools/build_comparison_md.py rename to Tools/review/build_comparison_md.py index 6193c2f..c31f243 100644 --- a/Tools/build_comparison_md.py +++ b/Tools/review/build_comparison_md.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Build the Blender-vs-in-game comparison page. - python3 Tools/build_comparison_md.py + python3 Tools/review/build_comparison_md.py Reads every in-game capture in `Screenshots/demo/` and every Blender render in `Screenshots/blender/`, pairs them by name, and writes `Screenshots/COMPARISON.md`: @@ -107,11 +107,11 @@ def build(): lines = [ "### Blender vs in-game", "", - "Regenerated by `Tools/build_comparison_md.py`; everything between the markers is", + "Regenerated by `Tools/review/build_comparison_md.py`; everything between the markers is", "rewritten, so edit outside them.", "", f"- Captures: `{REVIEW_DIR}/` and `{DEMO_DIR}/` ({len(shots)} files)", - f"- Blender renders: `{BLENDER_DIR}/` ({len(renders)} files), made by `Tools/blender_screenshots.py`", + f"- Blender renders: `{BLENDER_DIR}/` ({len(renders)} files), made by `Tools/blender/blender_screenshots.py`", "", "Left is what the engine draws, right is what Blender has. A gap between them is the", "import problem, not a taste problem: scale, material colour, missing parts and split", diff --git a/Tools/review/e2e_asset_check.py b/Tools/review/e2e_asset_check.py new file mode 100644 index 0000000..be25ceb --- /dev/null +++ b/Tools/review/e2e_asset_check.py @@ -0,0 +1,130 @@ +"""End-to-end asset check: Blender render -> Unreal build -> captures -> judge -> README. + + python3 Tools/review/e2e_asset_check.py # everything + python3 Tools/review/e2e_asset_check.py --skip-blender # reuse the existing Blender renders + python3 Tools/review/e2e_asset_check.py --skip-judge # no API calls, just the images + +The chain, in order, each step failing loudly with its log under /tmp/gigline/: + +1. **Blender renders** - `Art/blender/*/` assets rendered by `Tools/blender/blender_screenshots.py`, so the + source the imports came from has an image of its own. +2. **Empty maps** - `Tools/unreal/ue_recreate_maps.py` rebuilds both maps, so a run never inherits an actor + from an experiment. The builders also clear the level they load; this is the belt-and-braces step. +3. **Unreal build** - `Tools/unreal/ue_build_demo_map.py` (game map) and `Tools/unreal/ue_build_review_map.py` + (staging map for individual assets). +4. **Captures** - `Tools/review/render_screenshots.sh`, which renders both maps into `Screenshots/`. +5. **Judge** - every shot in `Tools/review/visual_targets.json` judged by **two models** through + `Tools/review/judge_screenshot.py --shots`, which reconciles agreement and reports disagreement. +6. **Review file** - `Tools/review/build_comparison_md.py` rewrites the Blender-vs-UE block in + `Screenshots/README.md`, then prettier formats it. + +The report lands in `/tmp/gigline/e2e-report.txt`; the exit code is non-zero if any step failed. +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +import time + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +ENGINE = os.environ.get("ENGINE", "/run/media/system/DATA/Engines/UE_5.8.4") +UPROJECT = os.path.join(ROOT, "Game", "GigLine", "GigLine.uproject") +BLENDER = os.environ.get( + "BLENDER_FILES", "/var/lib/flatpak/app/org.blender.Blender/x86_64/stable/active/files") +UNREAL = os.path.join(ENGINE, "Engine", "Binaries", "Linux", "UnrealEditor-Cmd") +REPORT = "/tmp/gigline/e2e-report.txt" + + +def run(step, cmd, env=None, timeout=None, result_file=None, expect=None): + """Run one step and record it. A step fails on a non-zero exit or a missing marker.""" + started = time.time() + print(f"== {step}") + try: + proc = subprocess.run(cmd, cwd=ROOT, env={**os.environ, **(env or {})}, + capture_output=True, text=True, timeout=timeout) + except subprocess.TimeoutExpired: + return step, False, f"timed out after {timeout}s" + elapsed = time.time() - started + if proc.returncode != 0: + tail = (proc.stderr or proc.stdout or "").strip().splitlines()[-3:] + return step, False, f"exit {proc.returncode} ({elapsed:.0f}s): {' / '.join(tail)}" + if result_file and expect: + if not os.path.exists(result_file): + return step, False, f"no result file {result_file}" + text = open(result_file, errors="replace").read() + if expect not in text: + return step, False, f"{os.path.basename(result_file)} lacks {expect!r}" + return step, True, f"{elapsed:.0f}s" + + +def unreal_script(script, timeout=1800): + return [UNREAL, UPROJECT, "-run=pythonscript", f"-script={os.path.join(ROOT, script)}", + "-nullrhi", "-unattended", "-nopause", "-stdout"] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--skip-blender", action="store_true", help="reuse the Blender renders") + parser.add_argument("--skip-judge", action="store_true", help="no API calls") + parser.add_argument("--skip-recreate", action="store_true", + help="keep the existing maps (the builders still clear their levels)") + args = parser.parse_args() + + steps = [] + if not args.skip_blender: + steps.append(run("1/6 blender renders", + [os.path.join(BLENDER, "blender", "blender"), "-b", "--factory-startup", + "--python", os.path.join(ROOT, "Tools", "blender", "blender_screenshots.py"), + "--", "--mode", "both", "--out", os.path.join(ROOT, "Screenshots", "blender")], + env={"LD_LIBRARY_PATH": os.path.join(BLENDER, "blender", "lib")}, + timeout=1800)) + else: + steps.append(("1/6 blender renders", True, "skipped, reusing Screenshots/blender")) + + if not args.skip_recreate: + steps.append(run("2/6 empty maps", unreal_script("Tools/unreal/ue_recreate_maps.py"), + timeout=900, result_file="/tmp/gigline/recreate-maps.txt", + expect="VERDICT: PASS")) + else: + steps.append(("2/6 empty maps", True, "skipped")) + + steps.append(run("3/6 unreal build: demo map", unreal_script("Tools/unreal/ue_build_demo_map.py"), + timeout=1800, result_file="/tmp/gigline/demo-map.txt", expect="save_map")) + steps.append(run("3/6 unreal build: review map", unreal_script("Tools/unreal/ue_build_review_map.py"), + timeout=1800, result_file="/tmp/ue-review-map-result.txt", expect="save_map")) + + steps.append(run("4/6 captures", ["bash", os.path.join(ROOT, "Tools", "review", "render_screenshots.sh")], + timeout=2400)) + + if not args.skip_judge: + steps.append(run("5/6 judge (two models)", + [sys.executable, os.path.join(ROOT, "Tools", "review", "judge_screenshot.py"), "--shots"], + timeout=1800)) + else: + steps.append(("5/6 judge (two models)", True, "skipped")) + + steps.append(run("6/6 review file", + [sys.executable, os.path.join(ROOT, "Tools", "review", "build_comparison_md.py")], + timeout=600)) + + lines = [f"e2e asset check {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}", ""] + ok = True + for step, passed, detail in steps: + lines.append(f"{'PASS' if passed else 'FAIL'} {step}: {detail}") + ok = ok and passed + lines += ["", f"VERDICT: {'PASS' if ok else 'FAIL'}", + "Review: Screenshots/README.md (status table at the top)."] + os.makedirs(os.path.dirname(REPORT), exist_ok=True) + with open(REPORT, "w") as handle: + handle.write("\n".join(lines) + "\n") + print() + print("\n".join(lines)) + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Tools/hero_compare.py b/Tools/review/hero_compare.py similarity index 97% rename from Tools/hero_compare.py rename to Tools/review/hero_compare.py index 7c188ab..d838bf7 100644 --- a/Tools/hero_compare.py +++ b/Tools/review/hero_compare.py @@ -1,6 +1,6 @@ """Build a side-by-side plate from a reference photograph and a rendered capture. - python3 Tools/hero_compare.py \ + python3 Tools/review/hero_compare.py \ --reference Art/reference/hero-masonic/02-masonictemple01.jpg \ --render Screenshots/review/review-hero-facade-front.png \ --out Screenshots/review/hero-compare-front.png diff --git a/Tools/hero_iterate.sh b/Tools/review/hero_iterate.sh similarity index 83% rename from Tools/hero_iterate.sh rename to Tools/review/hero_iterate.sh index f26068f..ed4f2ce 100755 --- a/Tools/hero_iterate.sh +++ b/Tools/review/hero_iterate.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Full hero-facade iteration: build -> import -> render -> judge -> comparison plate. # -# bash Tools/hero_iterate.sh [shot] +# bash Tools/review/hero_iterate.sh [shot] # # `shot` defaults to review-hero-facade-front. Every step writes to /tmp/gigline/ so a failure can be # read back: Unreal commandlets swallow print(). @@ -25,7 +25,7 @@ find Art -name '*.blend*' -exec chmod u+w {} \; step "2/6 build the facade" LD_LIBRARY_PATH="$BLENDER_FILES/blender/lib" "$BLENDER_FILES/blender/blender" -b --factory-startup \ - --python Tools/hero_facade_build.py -- \ + --python Tools/blender/hero_facade_build.py -- \ --out /tmp/gigline/hero-facade.fbx \ --save-blend Art/blender/hero/hero-facade-2026-11-07.blend \ --report Art/blender/hero/hero-facade-2026-11-07.json \ @@ -34,24 +34,24 @@ grep -E 'exported FBX' /tmp/gigline/hero-build.log || { echo "no FBX written"; e step "3/6 import into Unreal" "$ENGINE/Engine/Binaries/Linux/UnrealEditor-Cmd" "$UPROJECT" -run=pythonscript \ - -script="$(pwd)/Tools/ue_hero_facade_import.py" -nullrhi -unattended -nopause -stdout \ + -script="$(pwd)/Tools/unreal/ue_hero_facade_import.py" -nullrhi -unattended -nopause -stdout \ > /tmp/gigline/hero-import.log 2>&1 grep -q 'save_map' /tmp/gigline/hero-facade-import.txt || { echo "import failed - see /tmp/gigline/hero-facade-import.txt"; exit 1; } step "4/6 rebuild DemoMap (materials live in the map builder)" "$ENGINE/Engine/Binaries/Linux/UnrealEditor-Cmd" "$UPROJECT" -run=pythonscript \ - -script="$(pwd)/Tools/ue_build_demo_map.py" -nullrhi -unattended -nopause -stdout \ + -script="$(pwd)/Tools/unreal/ue_build_demo_map.py" -nullrhi -unattended -nopause -stdout \ > /tmp/gigline/hero-demomap.log 2>&1 grep -q 'save_map' /tmp/gigline/demo-map.txt || { echo "demo map rebuild failed - see /tmp/gigline/demo-map.txt"; exit 1; } step "5/6 render the capture set" -bash Tools/render_screenshots.sh > /tmp/gigline/hero-render.log 2>&1 || { echo "render failed - see /tmp/gigline/hero-render.log"; exit 1; } +bash Tools/review/render_screenshots.sh > /tmp/gigline/hero-render.log 2>&1 || { echo "render failed - see /tmp/gigline/hero-render.log"; exit 1; } step "6/6 judge and compare" -python3 Tools/judge_screenshot.py --image "Screenshots/review/${SHOT}.png" \ +python3 Tools/review/judge_screenshot.py --image "Screenshots/review/${SHOT}.png" \ --expect "A neoclassical Masonic temple facade: warm yellow-grey sandstone with horizontal ashlar courses, six giant fluted Ionic columns on square pedestals, three tall red vertical banners reading JIMMY KIMMEL LIVE hanging between the columns, dark lattice metal grilles in the window bays behind the colonnade, three recessed dark entrance doorways with steps, above the columns a carved inscription band flanked by two round medallions, topped by a crested parapet." \ | tee /tmp/gigline/hero-judge.txt | tail -2 -python3 Tools/hero_compare.py --reference "$REFERENCE" \ +python3 Tools/review/hero_compare.py --reference "$REFERENCE" \ --render "Screenshots/review/${SHOT}.png" \ --out "Screenshots/review/hero-compare-${SHOT#review-hero-}.png" diff --git a/Tools/judge_screenshot.py b/Tools/review/judge_screenshot.py similarity index 97% rename from Tools/judge_screenshot.py rename to Tools/review/judge_screenshot.py index 7992dd9..70a3b61 100755 --- a/Tools/judge_screenshot.py +++ b/Tools/review/judge_screenshot.py @@ -70,7 +70,7 @@ def combine(judgements): return {"verdict": verdict, "confidence": confidence, "reason": reason, "models": by_model} # Project root = parent of the Tools/ directory the script lives in. -PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) ENV_PATH = os.path.join(PROJECT_ROOT, ".env") @@ -149,14 +149,14 @@ def judge(api_key, image_path, expect, model, timeout=60): def parse_shots_from_targets(): - """Read the review shots and their expectations from Tools/visual_targets.json. + """Read the review shots and their expectations from Tools/review/visual_targets.json. Parsing the README's tables broke when the README was restructured into per-shot sections with the comparison table appended: the old parser looked for a table whose first cell held a backticked `review-` name. The targets file is the same source the render gate uses, so it is both stable and the thing the expectations are written to. """ - targets_path = os.path.join(PROJECT_ROOT, "Tools", "visual_targets.json") + targets_path = os.path.join(PROJECT_ROOT, "Tools", "review", "visual_targets.json") if not os.path.isfile(targets_path): sys.exit(f"error: no {targets_path}") import json @@ -195,7 +195,7 @@ def main(): if args.shots: shots = parse_shots_from_targets() if not shots: - sys.exit("error: no shots found in Tools/visual_targets.json") + sys.exit("error: no shots found in Tools/review/visual_targets.json") n = len(shots) print(f"Judging {n} screenshots with {' + '.join(models)} ...") ok = 0 diff --git a/Tools/render_screenshots.py b/Tools/review/render_screenshots.py similarity index 98% rename from Tools/render_screenshots.py rename to Tools/review/render_screenshots.py index 24f1cd5..d5c2e02 100644 --- a/Tools/render_screenshots.py +++ b/Tools/review/render_screenshots.py @@ -5,7 +5,7 @@ # angles synchronously via SceneCapture2D render targets, writing PNGs # directly into /Screenshots/ (gitignored). # -# Invoked by Tools/render_screenshots.sh (UnrealEditor-Cmd -run=pythonscript). +# Invoked by Tools/review/render_screenshots.sh (UnrealEditor-Cmd -run=pythonscript). # NOTE: no -nullrhi — rendering needs the real Vulkan RHI. In 5.8 the # commandlet swallows print(); results go to /tmp/ue-screenshots-result.txt. # Do NOT load_map here: after a map load the commandlet's spawn context dies diff --git a/Tools/render_screenshots.sh b/Tools/review/render_screenshots.sh similarity index 90% rename from Tools/render_screenshots.sh rename to Tools/review/render_screenshots.sh index c5603d1..d74067b 100755 --- a/Tools/render_screenshots.sh +++ b/Tools/review/render_screenshots.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash # Render the demo-map screenshots used for visual review into /Screenshots/demo/. # -# Usage: Tools/render_screenshots.sh [--verify] +# Usage: Tools/review/render_screenshots.sh [--verify] # # Only the **demo map** is captured: the review staging map exists to hold the # hero/camera list, not to produce review screenshots. Everything here is what a # player actually sees when the demo runs. # -# --verify run Tools/visual_loop.py --verify afterwards and fail if a shot +# --verify run Tools/review/visual_loop.py --verify afterwards and fail if a shot # does not show the content its expectation describes # # The PNGs are committed (see Screenshots/README.md): a reviewer must see exactly @@ -20,11 +20,13 @@ PROJ="$REPO/Game/GigLine/GigLine.uproject" VERIFY="no" [ "${1:-}" = "--verify" ] && VERIFY="yes" +# Tools are grouped by discipline now: Unreal commandlets under Tools/unreal, review helpers under +# Tools/review. The script name alone is no longer enough to find one, so each call names its folder. run_cmdlet() { - local script="$1" label="$2" + local script="$1" label="$2" folder="${3:-unreal}" echo "== $label ==" ( cd "$ENGINE" && ./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \ - -run=pythonscript -script="$REPO/Tools/$script" \ + -run=pythonscript -script="$REPO/Tools/$folder/$script" \ -nullrhi -unattended -nopause -stdout ) } @@ -107,5 +109,5 @@ fi if [ "$VERIFY" = "yes" ]; then echo "== verify: does each shot show what it should? ==" cd "$REPO" - python3 Tools/visual_loop.py --verify + python3 Tools/review/visual_loop.py --verify fi diff --git a/Tools/shadow_gap.py b/Tools/review/shadow_gap.py similarity index 98% rename from Tools/shadow_gap.py rename to Tools/review/shadow_gap.py index af3d715..50bf92b 100644 --- a/Tools/shadow_gap.py +++ b/Tools/review/shadow_gap.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Measure how far an object sits above its own shadow. - python3 Tools/shadow_gap.py --image Screenshots/demo/review-demo-spawn-view.png \ + python3 Tools/review/shadow_gap.py --image Screenshots/demo/review-demo-spawn-view.png \ --object-box 0.52,0.55,0.60,0.75 --out /tmp/gigline/shadow-gap.json Why a measurement instead of another opinion: a still cannot distinguish "the object floats" diff --git a/Tools/visual_loop.py b/Tools/review/visual_loop.py similarity index 97% rename from Tools/visual_loop.py rename to Tools/review/visual_loop.py index e0bb715..522cf8d 100644 --- a/Tools/visual_loop.py +++ b/Tools/review/visual_loop.py @@ -13,7 +13,7 @@ engine screenshot ─┘ │ It answers three questions with numbers instead of opinions: 1. **Does the shot show what it was supposed to?** (`--verify`) — every capture - has an expectation in `Tools/visual_targets.json`; a shot that is blank, the + has an expectation in `Tools/review/visual_targets.json`; a shot that is blank, the wrong size, or wildly unlike its expectation is reported as a failure. 2. **How far is the render from the reference?** (`--compare`) — luminance, contrast, edge density, palette and 3×3 composition are measured on both and @@ -26,9 +26,9 @@ Iterations are **not committed**: every artefact goes under `Art/reference/iterations//`, which is gitignored. Promote a shot into `Screenshots/` only after the owner confirms it. - python3 Tools/visual_loop.py --verify # gate the current set - python3 Tools/visual_loop.py --compare --iteration 3 # measure vs references - python3 Tools/visual_loop.py --compare --iteration 3 --judge # + vision advisory + python3 Tools/review/visual_loop.py --verify # gate the current set + python3 Tools/review/visual_loop.py --compare --iteration 3 # measure vs references + python3 Tools/review/visual_loop.py --compare --iteration 3 --judge # + vision advisory Metrics are computed with Pillow (already in requirements-tools.txt); no numpy. """ @@ -43,8 +43,8 @@ import sys from PIL import Image, ImageFilter, ImageStat -ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -TARGETS = os.path.join(ROOT, "Tools", "visual_targets.json") +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +TARGETS = os.path.join(ROOT, "Tools", "review", "visual_targets.json") ITER_ROOT = os.path.join(ROOT, "Art", "reference", "iterations") # A shot must clear these floors or it is treated as broken, not merely ugly. diff --git a/Tools/visual_targets.json b/Tools/review/visual_targets.json similarity index 100% rename from Tools/visual_targets.json rename to Tools/review/visual_targets.json diff --git a/Tools/tests/test_block_height_report.py b/Tools/tests/test_block_height_report.py index 2603398..f7e777e 100644 --- a/Tools/tests/test_block_height_report.py +++ b/Tools/tests/test_block_height_report.py @@ -4,6 +4,15 @@ The Geo tools live outside `Tools/`, so the test adds `Geo/tools` to `sys.path` before importing the module by name. """ +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import os import sys import unittest diff --git a/Tools/tests/test_character_match.py b/Tools/tests/test_character_match.py index 941f53e..8cbd32d 100644 --- a/Tools/tests/test_character_match.py +++ b/Tools/tests/test_character_match.py @@ -1,5 +1,14 @@ """Tests for the pure parts of Tools/character_match.py.""" +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import os import sys import unittest diff --git a/Tools/tests/test_comfyui_texture.py b/Tools/tests/test_comfyui_texture.py index 3362dc8..a33ae09 100644 --- a/Tools/tests/test_comfyui_texture.py +++ b/Tools/tests/test_comfyui_texture.py @@ -1,5 +1,14 @@ """Graph shape for the documented texture workflows (no ComfyUI process involved).""" +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import unittest from comfyui_generate import TEXTURE_PROFILES, texture_workflow diff --git a/Tools/tests/test_facade_detail_concepts.py b/Tools/tests/test_facade_detail_concepts.py index fae36ca..1d0898d 100644 --- a/Tools/tests/test_facade_detail_concepts.py +++ b/Tools/tests/test_facade_detail_concepts.py @@ -4,6 +4,15 @@ No network calls: the OpenRouter request and the image bytes are represented as the small structures the API uses. Generation itself is manual and paid. """ +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import base64 import json import os diff --git a/Tools/tests/test_fbx_export_preset.py b/Tools/tests/test_fbx_export_preset.py index 585f7be..9beb18a 100644 --- a/Tools/tests/test_fbx_export_preset.py +++ b/Tools/tests/test_fbx_export_preset.py @@ -5,6 +5,15 @@ The preset module keeps its argument set in plain dicts and builds it in Blender. Only `export_static`/`export_character` import `bpy`. """ +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import os import sys import unittest diff --git a/Tools/tests/test_input_emulation.py b/Tools/tests/test_input_emulation.py index db16fb7..693b462 100644 --- a/Tools/tests/test_input_emulation.py +++ b/Tools/tests/test_input_emulation.py @@ -5,6 +5,15 @@ covered here; the end-to-end injection is verified by running the tool (see Docs/release-checks.md and MEMORY.md). """ +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import unittest import input_emulation as ie diff --git a/Tools/tests/test_judge_combine.py b/Tools/tests/test_judge_combine.py index 205eb0e..e1bd25a 100644 --- a/Tools/tests/test_judge_combine.py +++ b/Tools/tests/test_judge_combine.py @@ -1,5 +1,14 @@ """Tests for the multi-model reconciliation in judge_screenshot.combine().""" +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import unittest from judge_screenshot import combine diff --git a/Tools/tests/test_mapbox_buildings.py b/Tools/tests/test_mapbox_buildings.py index 47690cf..64fbb0f 100644 --- a/Tools/tests/test_mapbox_buildings.py +++ b/Tools/tests/test_mapbox_buildings.py @@ -5,6 +5,15 @@ the API returns, and only the selection/comparison/formatting helpers are exercised. The live call is verified by running the tool. """ +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import json import os import sys diff --git a/Tools/tests/test_retail_frontage.py b/Tools/tests/test_retail_frontage.py index 412ec1b..2679311 100644 --- a/Tools/tests/test_retail_frontage.py +++ b/Tools/tests/test_retail_frontage.py @@ -4,6 +4,15 @@ The builder itself needs Blender; `bay_centres` does not, so the bay rhythm the façade depends on is checkable without starting Blender or Unreal. """ +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import unittest from Tools._common import bay_centres diff --git a/Tools/tests/test_shadow_gap.py b/Tools/tests/test_shadow_gap.py index b5285bf..ec23d4a 100644 --- a/Tools/tests/test_shadow_gap.py +++ b/Tools/tests/test_shadow_gap.py @@ -1,5 +1,14 @@ """Tests for the pure parts of Tools/shadow_gap.py.""" +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import os import sys import unittest diff --git a/Tools/tests/test_ue_common.py b/Tools/tests/test_ue_common.py index e2d2b28..698d31c 100644 --- a/Tools/tests/test_ue_common.py +++ b/Tools/tests/test_ue_common.py @@ -4,6 +4,15 @@ Pure stdlib: these helpers exist to turn two silent failures into loud ones, so they are tested without starting Unreal. """ +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import os import stat import sys diff --git a/Tools/tests/test_ue_placement.py b/Tools/tests/test_ue_placement.py index 6835a51..42651c1 100644 --- a/Tools/tests/test_ue_placement.py +++ b/Tools/tests/test_ue_placement.py @@ -6,6 +6,15 @@ Running the importer moved the hero 13 m east and onto the north side of the boulevard, and nothing complained because a displaced building still renders. """ +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import os import sys import unittest @@ -70,14 +79,14 @@ class SingleSourceOfTruthTest(unittest.TestCase): """The import script and the demo builder must not carry their own literals.""" def test_import_script_uses_the_shared_constant(self): - src = open(os.path.join(ROOT, "Tools", "ue_hero_facade_import.py"), + src = open(os.path.join(ROOT, "Tools", "unreal", "ue_hero_facade_import.py"), encoding="utf-8").read() self.assertIn("ue_placement.HERO_LOCATION_CM", src) self.assertNotIn("(-12024.5, -1450.0, 0.0)", src, "the stale pre-0.1.0 hero position must not come back") def test_demo_builder_uses_the_shared_constant(self): - src = open(os.path.join(ROOT, "Tools", "ue_build_demo_map.py"), + src = open(os.path.join(ROOT, "Tools", "unreal", "ue_build_demo_map.py"), encoding="utf-8").read() self.assertIn("ue_placement.HERO_LOCATION_CM", src) diff --git a/Tools/tests/test_visual_loop.py b/Tools/tests/test_visual_loop.py index f7e4210..20da20f 100644 --- a/Tools/tests/test_visual_loop.py +++ b/Tools/tests/test_visual_loop.py @@ -5,6 +5,15 @@ numbers can be reasoned about directly (a mid-grey flat frame has near-zero edge density and contrast; a checkerboard has a lot of both). """ +import os +import sys + +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _TOOLS) +for _sub in ("review", "gen", "unreal", "blender", "input"): + sys.path.insert(0, os.path.join(_TOOLS, _sub)) + + import os import sys import tempfile diff --git a/Tools/ue_apply_facade_kit.py b/Tools/unreal/ue_apply_facade_kit.py similarity index 96% rename from Tools/ue_apply_facade_kit.py rename to Tools/unreal/ue_apply_facade_kit.py index 0cf9ac7..640a6c9 100644 --- a/Tools/ue_apply_facade_kit.py +++ b/Tools/unreal/ue_apply_facade_kit.py @@ -10,7 +10,7 @@ blockout gives: the bay count is `round(width / BAY)` and the storey count comes from the height above the ground floor. Nothing is hand-placed per building. UnrealEditor-Cmd -run=pythonscript \ - -script=Tools/ue_apply_facade_kit.py -nullrhi -unattended -nopause -stdout + -script=Tools/unreal/ue_apply_facade_kit.py -nullrhi -unattended -nopause -stdout # or, as a step inside the demo builder: from ue_apply_facade_kit import apply_to_actor @@ -25,7 +25,7 @@ import sys import unreal -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ue_common # noqa: E402 KIT = "/Game/Hollywood/Kit" @@ -206,11 +206,11 @@ def apply_to_actor(actor, kit, materials=None, prefix=None): def main() -> None: kit = load_kit() if not kit: - log(f"no kit meshes under {KIT} — run Tools/ue_import_facade_kit.py first") + log(f"no kit meshes under {KIT} — run Tools/unreal/ue_import_facade_kit.py first") return log(f"kit meshes: {len(kit)}") - # Site materials built by Tools/ue_build_demo_map.py. + # Site materials built by Tools/unreal/ue_build_demo_map.py. materials = {} for name in ("M_FacadeStone", "M_FacadeStoneShadow", "M_FacadeGlass", "M_FacadeBanner", "M_Stucco", "M_Concrete"): @@ -237,7 +237,7 @@ def main() -> None: # The kit is the fallback for buildings that have nothing but a blockout. These # two are modelled from their own photograph in 0.1.6 - # (Tools/retail_frontage_build.py); pasting the generic kit over them put a + # (Tools/blender/retail_frontage_build.py); pasting the generic kit over them put a # second, contradicting facade language on the same wall. MODELLED = ("Frontage_SM_LeeDrug_6800", "Frontage_SM_Hollywood_6806") targets = [a for a in unreal.EditorLevelLibrary.get_all_level_actors() diff --git a/Tools/ue_audit_scene.py b/Tools/unreal/ue_audit_scene.py similarity index 93% rename from Tools/ue_audit_scene.py rename to Tools/unreal/ue_audit_scene.py index cf05e54..23f1521 100644 --- a/Tools/ue_audit_scene.py +++ b/Tools/unreal/ue_audit_scene.py @@ -7,13 +7,13 @@ Checks that are cheap and catch the failures this project actually had: because the demo builder deleted and recreated the site materials, orphaning every reference; the map looked merely "a bit flat". 2. **Placement spans.** Each verified frontage building is probed against - `Tools/ue_placement.py`; a building 13 m off its footprint still renders fine, + `Tools/unreal/ue_placement.py`; a building 13 m off its footprint still renders fine, which is exactly why it needs a number, not an eye. 3. **No duplicate actor labels.** Two buildings' kit pieces sharing a label made the outliner ambiguous and hid one of them. UnrealEditor-Cmd -run=pythonscript \ - -script=Tools/ue_audit_scene.py -nullrhi -unattended -nopause -stdout + -script=Tools/unreal/ue_audit_scene.py -nullrhi -unattended -nopause -stdout Result file: `/tmp/gigline/scene-audit.txt`. Exit code from the commandlet is always 0, so **read the file** — `visual_loop.py --verify` does not depend on it. @@ -27,7 +27,7 @@ import sys import unreal -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ue_placement # noqa: E402 MAP = "/Game/Hollywood/DemoMap" diff --git a/Tools/ue_build_demo_map.py b/Tools/unreal/ue_build_demo_map.py similarity index 96% rename from Tools/ue_build_demo_map.py rename to Tools/unreal/ue_build_demo_map.py index 5b29b22..90e0424 100644 --- a/Tools/ue_build_demo_map.py +++ b/Tools/unreal/ue_build_demo_map.py @@ -1,11 +1,11 @@ # Build /Game/Hollywood/DemoMap: the walkable demo level. # # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_build_demo_map.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_build_demo_map.py -nullrhi -unattended -nopause -stdout # # Content this map expects (built by earlier steps): -# /Game/Hollywood/ElCapitan/SM_HeroFacade Tools/ue_hero_facade_import.py -# /Game/Hollywood/Site/... Tools/ue_import_site.py +# /Game/Hollywood/ElCapitan/SM_HeroFacade Tools/unreal/ue_hero_facade_import.py +# /Game/Hollywood/Site/... Tools/unreal/ue_import_site.py # # Map conventions, from Geo/manifests/local-origin.md: # X east, Y south, Z up, centimetres, origin at Hollywood x Highland. @@ -22,8 +22,8 @@ import unreal MAP = "/Game/Hollywood/DemoMap" # Repository root, so the texture generator can be invoked by path. -ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ue_placement # noqa: E402 RESULT = "/tmp/gigline/demo-map.txt" lines = [] @@ -284,6 +284,25 @@ def ensure_material(mat_name, spec, textures=None): return mat +def clear_all(): + """Destroy every actor in the level. + + The prefix-based clear only removed what this builder knows about, so anything an experiment left behind + survived every rebuild - that is how a stale road mesh and a duplicate vehicle actor both ended up saved + back into the map. Every build now starts from an empty world. + """ + removed = 0 + for actor in list(unreal.EditorLevelLibrary.get_all_level_actors()): + label = actor.get_actor_label() + try: + unreal.EditorLevelLibrary.destroy_actor(actor) + removed += 1 + _ = label + except Exception: # noqa: BLE001 - a locked actor must not abort the clear + log(f"could not destroy {label}") + return removed + + def clear(prefixes=()): removed = 0 for actor in unreal.EditorLevelLibrary.get_all_level_actors(): @@ -329,14 +348,8 @@ def build(): # Repeat until nothing is left: destroying actors while iterating a snapshot can miss # some, which is how two Demo_Vehicle_Sedan actors survived into a saved map and the # scene audit failed on a duplicate label. - removed = 0 - for _ in range(4): - pass_removed = clear(("Demo_", "PlayerStart", "Hero_Facade", "Frontage_", - "RetailTrim_", "ShotSequencer")) - removed += pass_removed - if pass_removed == 0: - break - log(f"cleared {removed} previous demo actors") + removed = clear_all() + log(f"cleared {removed} previous actors (whole level, not just known prefixes)") # --- lighting: fixed daytime ------------------------------------------ # The demo is a fixed daytime scene, not a time-of-day simulation. Three @@ -448,7 +461,7 @@ def build(): actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC) log("staged: Hero_Facade at the building line") else: - log("MISSING: SM_HeroFacade — run Tools/ue_hero_facade_import.py") + log("MISSING: SM_HeroFacade — run Tools/unreal/ue_hero_facade_import.py") # --- site meshes, if they have been imported -------------------------- # Blender's flat diffuse colours do not survive FBX -> Unreal: Interchange @@ -637,7 +650,7 @@ def build(): for asset_name, label in FRONTAGE: mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Frontage/{asset_name}") if mesh is None: - log(f"MISSING frontage {asset_name} — run Tools/ue_import_block_frontage.py") + log(f"MISSING frontage {asset_name} — run Tools/unreal/ue_import_block_frontage.py") continue actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"Frontage_{asset_name}") if actor: @@ -660,7 +673,7 @@ def build(): ("SM_Retail_6806", "6806-6820 trim")): mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Frontage/{asset_name}") if mesh is None: - log(f"MISSING trim {asset_name} — run Tools/ue_import_retail_detail.py") + log(f"MISSING trim {asset_name} — run Tools/unreal/ue_import_retail_detail.py") continue # RetailTrim_ rather than Frontage_ so the kit assembler leaves it alone. actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"RetailTrim_{asset_name}") diff --git a/Tools/ue_build_review_map.py b/Tools/unreal/ue_build_review_map.py similarity index 95% rename from Tools/ue_build_review_map.py rename to Tools/unreal/ue_build_review_map.py index 79c992c..8b82142 100644 --- a/Tools/ue_build_review_map.py +++ b/Tools/unreal/ue_build_review_map.py @@ -1,11 +1,11 @@ # Builds /Game/Test/ReviewMap — the visual-review scene — and saves it as a -# map asset. Run before the screenshot pass (Tools/render_screenshots.sh does +# map asset. Run before the screenshot pass (Tools/review/render_screenshots.sh does # this automatically). # # Scene: metric floor, key light, fill sky, sky atmosphere, MPFB character, # the three 1 m scale cubes, and an AGigLineShotActor with the camera list. # -# UnrealEditor-Cmd -run=pythonscript -script=/Tools/ue_build_review_map.py \ +# UnrealEditor-Cmd -run=pythonscript -script=/Tools/unreal/ue_build_review_map.py \ # -nullrhi -unattended -nopause -stdout # # Do NOT load_map here: after a map load the commandlet's spawn context dies @@ -49,6 +49,23 @@ def ensure_writable(path): pass +def clear_level(): + """Destroy every actor before staging. + + Without this the review map accumulated whatever previous runs and experiments left behind, and the + captures showed a scene nobody staged. The map is rebuilt from empty on every run. + """ + removed = 0 + for actor in list(unreal.EditorLevelLibrary.get_all_level_actors()): + try: + unreal.EditorLevelLibrary.destroy_actor(actor) + removed += 1 + except Exception: # noqa: BLE001 + pass + log(f"cleared {removed} actors before staging") + return removed + + def sit_on_floor(actor, floor_z=2.0): """Shift an actor so the mesh's lowest point lands on `floor_z`. @@ -85,6 +102,10 @@ def spawn(cls, loc, label=None, scale=None, rot=None): # --- scene ------------------------------------------------------------------- try: + # Start from an empty level: the review map used to keep whatever previous runs left behind, and + # captures showed a scene nobody staged. + clear_level() + # Neutral gray floor material (the scale-test mesh ships red) floor_mat = None if not unreal.EditorAssetLibrary.does_asset_exist("/Game/Test/M_ReviewFloor"): @@ -205,7 +226,7 @@ try: actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC) log("staged: hero facade at the measured building line") else: - log("MISSING: /Game/Hollywood/ElCapitan/SM_HeroFacade (run Tools/ue_hero_facade_import.py)") + log("MISSING: /Game/Hollywood/ElCapitan/SM_HeroFacade (run Tools/unreal/ue_hero_facade_import.py)") # --- player pawn --------------------------------------------------------- # The review rounds must show the pawn the game actually spawns, not only the @@ -222,7 +243,7 @@ try: # --- vehicle, from the latest Blender import ------------------------------ # Staged from the same assets the demo map uses, so the review always shows what - # Tools/vehicle_from_template.py most recently produced rather than a copy that can + # Tools/blender/vehicle_from_template.py most recently produced rather than a copy that can # drift. The panels are plain static meshes because the vehicle pawn keeps its # physics only. vehicle_origin = unreal.Vector(-1600.0, 600.0, 0.0) @@ -417,7 +438,7 @@ try: unreal.EditorLevelLibrary.get_editor_world(), "/Game/Hollywood/DemoMap") log(f"demo map staged: {len(demo_shots)} cameras, save {saved}") else: - log("MISSING: /Game/Hollywood/DemoMap (run Tools/ue_build_demo_map.py)") + log("MISSING: /Game/Hollywood/DemoMap (run Tools/unreal/ue_build_demo_map.py)") except Exception as exc: # noqa log(f"staging failed: {exc!r}") diff --git a/Tools/ue_character_anim_import.py b/Tools/unreal/ue_character_anim_import.py similarity index 92% rename from Tools/ue_character_anim_import.py rename to Tools/unreal/ue_character_anim_import.py index 61cf4e7..4271197 100644 --- a/Tools/ue_character_anim_import.py +++ b/Tools/unreal/ue_character_anim_import.py @@ -1,9 +1,9 @@ # Import the character animation clips into Unreal (0.2.0). # # blender -b Art/blender/character/character-2026-11-07.blend \ -# --python Tools/character_anim_build.py -- --out /tmp/gigline/character-anim.fbx +# --python Tools/blender/character_anim_build.py -- --out /tmp/gigline/character-anim.fbx # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_character_anim_import.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_character_anim_import.py -nullrhi -unattended -nopause -stdout # # Imports the animated FBX next to the existing character assets and verifies the # result rather than assuming it: an FBX whose export omitted `bake_anim` imports @@ -31,7 +31,7 @@ def log(msg): def main(): if not os.path.exists(SRC): - log(f"missing {SRC}; run Tools/character_anim_build.py first") + log(f"missing {SRC}; run Tools/blender/character_anim_build.py first") return task = unreal.AssetImportTask() diff --git a/Tools/ue_character_clothing_import.py b/Tools/unreal/ue_character_clothing_import.py similarity index 92% rename from Tools/ue_character_clothing_import.py rename to Tools/unreal/ue_character_clothing_import.py index 827a8d9..c6249f6 100644 --- a/Tools/ue_character_clothing_import.py +++ b/Tools/unreal/ue_character_clothing_import.py @@ -1,9 +1,9 @@ # Import the garment meshes as separate skeletal meshes (0.2.2). # # blender -b Art/blender/character/character-2026-11-07.blend \ -# --python Tools/character_clothing_build.py -- --out /tmp/gigline/clothing +# --python Tools/blender/character_clothing_build.py -- --out /tmp/gigline/clothing # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_character_clothing_import.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_character_clothing_import.py -nullrhi -unattended -nopause -stdout # # Each garment is its own asset, per the character-artist skill's rule that clothing # stays separate so it can be swapped or re-materialled. The pawn attaches each one as diff --git a/Tools/ue_character_hair_import.py b/Tools/unreal/ue_character_hair_import.py similarity index 91% rename from Tools/ue_character_hair_import.py rename to Tools/unreal/ue_character_hair_import.py index f387e25..7823edd 100644 --- a/Tools/ue_character_hair_import.py +++ b/Tools/unreal/ue_character_hair_import.py @@ -1,9 +1,9 @@ # Import the hair cards as their own skeletal mesh (0.2.2). # # blender -b Art/blender/character/character-2026-11-07.blend \ -# --python Tools/character_hair_build.py -- --out /tmp/gigline/character-hair.fbx +# --python Tools/blender/character_hair_build.py -- --out /tmp/gigline/character-hair.fbx # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_character_hair_import.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_character_hair_import.py -nullrhi -unattended -nopause -stdout # # The hair is a separate asset on purpose: Unreal collapses the second material slot # when hair is joined into the body mesh, so a card system has to be its own object diff --git a/Tools/ue_character_import.py b/Tools/unreal/ue_character_import.py similarity index 99% rename from Tools/ue_character_import.py rename to Tools/unreal/ue_character_import.py index 9e93b82..46c5100 100644 --- a/Tools/ue_character_import.py +++ b/Tools/unreal/ue_character_import.py @@ -1,7 +1,7 @@ # Unreal-side half of the MPFB character acceptance: imports the skeletal FBX # with the named import preset "ue_character_v1" and verifies the result. # -# UnrealEditor-Cmd -run=pythonscript -script=/Tools/ue_character_import.py \ +# UnrealEditor-Cmd -run=pythonscript -script=/Tools/unreal/ue_character_import.py \ # -nullrhi -unattended -nopause -stdout import unreal diff --git a/Tools/ue_character_materials.py b/Tools/unreal/ue_character_materials.py similarity index 97% rename from Tools/ue_character_materials.py rename to Tools/unreal/ue_character_materials.py index 218f36f..e173215 100644 --- a/Tools/ue_character_materials.py +++ b/Tools/unreal/ue_character_materials.py @@ -1,8 +1,8 @@ # Wire the generated character textures into Unreal materials (0.2.2). # -# python3 Tools/generate_character_textures.py --out Art/textures/generated +# python3 Tools/gen/generate_character_textures.py --out Art/textures/generated # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_character_materials.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_character_materials.py -nullrhi -unattended -nopause -stdout # # Why new materials instead of editing the imported ones: # diff --git a/Tools/ue_clear_simple_collision.py b/Tools/unreal/ue_clear_simple_collision.py similarity index 96% rename from Tools/ue_clear_simple_collision.py rename to Tools/unreal/ue_clear_simple_collision.py index ef925e7..12d7a66 100644 --- a/Tools/ue_clear_simple_collision.py +++ b/Tools/unreal/ue_clear_simple_collision.py @@ -1,7 +1,7 @@ """Remove the auto-generated simple collision from the site meshes, keeping their triangles. /UnrealEditor-Cmd -run=pythonscript \ - -script=/Tools/ue_clear_simple_collision.py -nullrhi -unattended -nopause -stdout + -script=/Tools/unreal/ue_clear_simple_collision.py -nullrhi -unattended -nopause -stdout Why: the FBX import runs with `auto_generate_collision`, which adds ONE convex hull per mesh. For a 279 m terrain or road that hull is a giant slab, and any *physics body* inside it - the Chaos vehicle - diff --git a/Tools/ue_common.py b/Tools/unreal/ue_common.py similarity index 95% rename from Tools/ue_common.py rename to Tools/unreal/ue_common.py index 998646a..7ce6b48 100644 --- a/Tools/ue_common.py +++ b/Tools/unreal/ue_common.py @@ -6,7 +6,7 @@ is swallowed and a failed save reports `False` rather than raising. Both are tra that cost time in `0.1.x`, so they live here once instead of in each script. import os, sys - sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ue_common ue_common.make_map_writable("/run/media/.../Content/Hollywood/DemoMap.umap") diff --git a/Tools/ue_create_input_assets.py b/Tools/unreal/ue_create_input_assets.py similarity index 98% rename from Tools/ue_create_input_assets.py rename to Tools/unreal/ue_create_input_assets.py index 6b22eb0..a401a3e 100644 --- a/Tools/ue_create_input_assets.py +++ b/Tools/unreal/ue_create_input_assets.py @@ -6,7 +6,7 @@ # # Run headless (imports/dirty packages are saved before exit): # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_create_input_assets.py -nullrhi -unattended -nopause \ +# -script=/Tools/unreal/ue_create_input_assets.py -nullrhi -unattended -nopause \ # -stdout -FullStdOutLogOutput # # The result goes to /tmp/gigline/input-assets.txt because 5.8's pythonscript diff --git a/Tools/ue_hero_facade_import.py b/Tools/unreal/ue_hero_facade_import.py similarity index 97% rename from Tools/ue_hero_facade_import.py rename to Tools/unreal/ue_hero_facade_import.py index a31f6f7..985b6fe 100644 --- a/Tools/ue_hero_facade_import.py +++ b/Tools/unreal/ue_hero_facade_import.py @@ -2,7 +2,7 @@ # and places it at the measured site position. # # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_hero_facade_import.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_hero_facade_import.py -nullrhi -unattended -nopause -stdout # # Placement follows Geo/manifests/local-origin.md and the blockout frame: # project X = UTM easting - 376515.389 (east +) @@ -17,7 +17,7 @@ import sys import unreal -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ue_common # noqa: E402 import ue_placement # noqa: E402 @@ -27,7 +27,7 @@ ACTOR_LABEL = ue_placement.HERO_ACTOR LEVEL = "/Game/Hollywood/DemoMap" # Unreal centimetres: south-east corner of the façade, on the building line. -# Placement lives in Tools/ue_placement.py. It used to be a literal here, and it +# Placement lives in Tools/unreal/ue_placement.py. It used to be a literal here, and it # drifted from ue_build_demo_map.py: this file kept the pre-0.1.0 position # (-12024.5, -1450) while the builder staged (-13336, 1307), so running the importer # on its own moved the hero 13 m east and onto the north side of the boulevard. @@ -41,7 +41,7 @@ def log(msg): lines.append(str(msg)) -# Generated with Tools/comfyui_generate.py texture (AlbedoBase XL, see the sidecar beside the PNG). The +# Generated with Tools/gen/comfyui_generate.py texture (AlbedoBase XL, see the sidecar beside the PNG). The # reference facade is coursed rubble-ashlar, not a flat tint, and a texture is what carries that at 45 m. FACADE_ALBEDO = "Art/textures/generated/T_FacadeAshlar_AI.png" FACADE_ALBEDO_ASSET = "T_FacadeAshlar_AI" diff --git a/Tools/ue_import_block_frontage.py b/Tools/unreal/ue_import_block_frontage.py similarity index 88% rename from Tools/ue_import_block_frontage.py rename to Tools/unreal/ue_import_block_frontage.py index 8f22e45..a393b30 100644 --- a/Tools/ue_import_block_frontage.py +++ b/Tools/unreal/ue_import_block_frontage.py @@ -1,14 +1,14 @@ # Import the south-side block frontage FBX set into Unreal. # -# blender ... --python Tools/export_block_frontage.py -- --out /tmp/gigline/frontage +# blender ... --python Tools/blender/export_block_frontage.py -- --out /tmp/gigline/frontage # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_import_block_frontage.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_import_block_frontage.py -nullrhi -unattended -nopause -stdout # # Each mesh already sits at its world position in the project frame (the FBX was # exported without moving the object), so the actor is spawned at the origin and # the geometry carries the offset. Destination: /Game/Hollywood/Frontage/. # -# Staging into the DemoMap is done by Tools/ue_build_demo_map.py, which also +# Staging into the DemoMap is done by Tools/unreal/ue_build_demo_map.py, which also # assigns the look materials; this script only produces the assets. import glob @@ -73,7 +73,7 @@ def import_fbx(path, name): def main(): files = sorted(glob.glob(os.path.join(SRC, "*.fbx"))) if not files: - log(f"no FBX in {SRC}; run Tools/export_block_frontage.py first") + log(f"no FBX in {SRC}; run Tools/blender/export_block_frontage.py first") return for path in files: name = os.path.splitext(os.path.basename(path))[0] diff --git a/Tools/ue_import_facade_kit.py b/Tools/unreal/ue_import_facade_kit.py similarity index 91% rename from Tools/ue_import_facade_kit.py rename to Tools/unreal/ue_import_facade_kit.py index a3d50e1..82b74a7 100644 --- a/Tools/ue_import_facade_kit.py +++ b/Tools/unreal/ue_import_facade_kit.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 """Import the façade module kit into Unreal: /Game/Hollywood/Kit/*. -Pairs with `Tools/facade_kit_build.py`. Each piece is imported as its own static +Pairs with `Tools/blender/facade_kit_build.py`. Each piece is imported as its own static mesh at the module's origin (bottom-front centre), so a building can place a piece by bay index and storey index with a single offset. UnrealEditor-Cmd -run=pythonscript \ - -script=Tools/ue_import_facade_kit.py -nullrhi -unattended -nopause -stdout + -script=Tools/unreal/ue_import_facade_kit.py -nullrhi -unattended -nopause -stdout Result file: /tmp/gigline/kit-import.txt (the commandlet swallows print()). """ @@ -18,7 +18,7 @@ import sys import unreal -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ue_common # noqa: E402 SRC = "/tmp/gigline/kit" @@ -35,7 +35,7 @@ def log(message: str) -> None: def main() -> None: if not os.path.isdir(SRC): - log(f"MISSING source directory {SRC} — run Tools/facade_kit_build.py first") + log(f"MISSING source directory {SRC} — run Tools/blender/facade_kit_build.py first") return files = sorted(f for f in os.listdir(SRC) if f.lower().endswith(".fbx")) if not files: diff --git a/Tools/ue_import_north_buildings.py b/Tools/unreal/ue_import_north_buildings.py similarity index 96% rename from Tools/ue_import_north_buildings.py rename to Tools/unreal/ue_import_north_buildings.py index f65999a..1d8ea07 100644 --- a/Tools/ue_import_north_buildings.py +++ b/Tools/unreal/ue_import_north_buildings.py @@ -9,14 +9,14 @@ # (0,0,0) like the site meshes, not at the pivot-shifted hero facade position. # # UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_import_north_buildings.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_import_north_buildings.py -nullrhi -unattended -nopause -stdout import os import sys import unreal -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ue_common # noqa: E402 SRC = "/tmp/gigline/north" diff --git a/Tools/ue_import_retail_detail.py b/Tools/unreal/ue_import_retail_detail.py similarity index 91% rename from Tools/ue_import_retail_detail.py rename to Tools/unreal/ue_import_retail_detail.py index 313646e..91a97c6 100644 --- a/Tools/ue_import_retail_detail.py +++ b/Tools/unreal/ue_import_retail_detail.py @@ -1,9 +1,9 @@ # Import the retail frontage trim (6800 Lee Drug, 6806–6820) into Unreal. # -# blender -b --factory-startup --python Tools/retail_frontage_build.py -- \ +# blender -b --factory-startup --python Tools/blender/retail_frontage_build.py -- \ # --out /tmp/gigline/retail --report /tmp/gigline/retail-report.json # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_import_retail_detail.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_import_retail_detail.py -nullrhi -unattended -nopause -stdout # # Two meshes arrive, one per building, each already at its world position in the # project frame (the FBX carries the offset), so staging spawns at the origin. @@ -76,7 +76,7 @@ def import_fbx(path, name): def main(): files = sorted(glob.glob(os.path.join(SRC, "*.fbx"))) if not files: - log(f"no FBX in {SRC}; run Tools/retail_frontage_build.py first") + log(f"no FBX in {SRC}; run Tools/blender/retail_frontage_build.py first") return for path in files: import_fbx(path, os.path.splitext(os.path.basename(path))[0]) diff --git a/Tools/ue_import_site.py b/Tools/unreal/ue_import_site.py similarity index 98% rename from Tools/ue_import_site.py rename to Tools/unreal/ue_import_site.py index 43fb7cf..8e0c77a 100644 --- a/Tools/ue_import_site.py +++ b/Tools/unreal/ue_import_site.py @@ -2,7 +2,7 @@ # # blender ... --python Art/blender/site/export_site_fbx.py -- --out /tmp/gigline/site # /.../UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_import_site.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_import_site.py -nullrhi -unattended -nopause -stdout # # Each mesh is a separate FBX because they carry different materials and want # different collision settings (terrain and street are walkable surfaces). diff --git a/Tools/ue_placement.py b/Tools/unreal/ue_placement.py similarity index 96% rename from Tools/ue_placement.py rename to Tools/unreal/ue_placement.py index 9d20228..929adb4 100644 --- a/Tools/ue_placement.py +++ b/Tools/unreal/ue_placement.py @@ -9,7 +9,7 @@ boulevard** — and nothing complained, because a displaced building still rende The values are Unreal centimetres, absolute world space. Every one of them is verified against `Geo/verification/2026-11-06-el-capitan-block.md` and probed by -`Tools/visual_loop.py --verify` (via the demo screenshot expectations) and by the +`Tools/review/visual_loop.py --verify` (via the demo screenshot expectations) and by the world-span probes recorded in `MEMORY.md`. Keep this file boring: constants and one validator, no `unreal` import, so it can diff --git a/Tools/unreal/ue_recreate_maps.py b/Tools/unreal/ue_recreate_maps.py new file mode 100644 index 0000000..922df27 --- /dev/null +++ b/Tools/unreal/ue_recreate_maps.py @@ -0,0 +1,86 @@ +"""Rebuild both Unreal maps from an empty level, deleting whatever was there before. + + /UnrealEditor-Cmd -run=pythonscript \ + -script=/Tools/unreal/ue_recreate_maps.py -nullrhi -unattended -nopause -stdout + +Why: `Tools/unreal/ue_build_demo_map.py` and `Tools/unreal/ue_build_review_map.py` load the existing map and destroy the +actors they know about. Anything else that accumulated - an actor from an experiment, a material override, a +stale world setting - survives every rebuild, which is how a stale road mesh and a duplicated vehicle actor +both got saved back into the map. Deleting the `.umap` and creating an empty level makes every run start from +the same place. + +The umap files are LFS-tracked and check out read-only, so chmod comes first; and a level cannot be deleted +while it is the one the editor has open, so a scratch level is created to release it. + +Result: /tmp/gigline/recreate-maps.txt +""" + +import os + +import unreal + +MAPS = ("/Game/Hollywood/DemoMap", "/Game/Test/ReviewMap") +SCRATCH = "/Game/Test/_RecreateScratch" +RESULT = "/tmp/gigline/recreate-maps.txt" +lines = [] + + +def log(message): + lines.append(str(message)) + unreal.log(str(message)) + + +def make_writable(path): + """A .umap under Content/ that is read-only cannot be deleted; LFS checks it out that way.""" + disk = os.path.join(unreal.Paths.project_content_dir(), + path.replace("/Game/", "") + ".umap") + try: + mode = os.stat(disk).st_mode & 0o777 + if not mode & 0o200: + os.chmod(disk, 0o644) + log(f"{path}: chmod u+w ({oct(mode)} -> 0o644)") + except FileNotFoundError: + pass + + +def main(): + les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) + scratch_made = False + for path in MAPS: + make_writable(path) + current = unreal.EditorLevelLibrary.get_editor_world() + current_path = current.get_path_name().split(".")[0] if current else "" + if path == current_path: + # The editor cannot delete the map it has open: park it on a scratch level first. + if not scratch_made: + les.new_level(SCRATCH) + scratch_made = True + else: + les.load_level(SCRATCH) + log(f"{path}: released by switching to {SCRATCH}") + if unreal.EditorAssetLibrary.does_asset_exist(path): + if unreal.EditorAssetLibrary.delete_asset(path): + log(f"{path}: deleted") + else: + log(f"{path}: could not delete") + if les.new_level(path): + log(f"{path}: created empty") + else: + log(f"{path}: FAILED to create") + if unreal.EditorAssetLibrary.does_asset_exist(SCRATCH): + unreal.EditorAssetLibrary.delete_asset(SCRATCH) + ok = all(f"{path}: created empty" in line for path in MAPS for line in lines) + lines.append(f"VERDICT: {'PASS' if ok else 'FAIL'}") + log(f"recreate {'ok' if ok else 'FAILED'}") + + +try: + main() +except Exception as exc: # noqa: BLE001 + import traceback + log(f"FAILED: {type(exc).__name__}: {exc}") + log(traceback.format_exc()) + lines.append("VERDICT: FAIL") + +with open(RESULT, "w") as handle: + handle.write("\n".join(lines) + "\n") diff --git a/Tools/ue_scale_test_import.py b/Tools/unreal/ue_scale_test_import.py similarity index 95% rename from Tools/ue_scale_test_import.py rename to Tools/unreal/ue_scale_test_import.py index 8e8c5dd..6391e63 100644 --- a/Tools/ue_scale_test_import.py +++ b/Tools/unreal/ue_scale_test_import.py @@ -3,7 +3,7 @@ # # Run headless: # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_scale_test_import.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_scale_test_import.py -nullrhi -unattended -nopause -stdout import unreal diff --git a/Tools/ue_stage_theatre_trim.py b/Tools/unreal/ue_stage_theatre_trim.py similarity index 95% rename from Tools/ue_stage_theatre_trim.py rename to Tools/unreal/ue_stage_theatre_trim.py index 5448ddc..001468b 100644 --- a/Tools/ue_stage_theatre_trim.py +++ b/Tools/unreal/ue_stage_theatre_trim.py @@ -10,7 +10,7 @@ Two steps, both idempotent: happen — the kit was once placed 12 km away that way. UnrealEditor-Cmd -run=pythonscript \ - -script=Tools/ue_stage_theatre_trim.py -nullrhi -unattended -nopause -stdout + -script=Tools/unreal/ue_stage_theatre_trim.py -nullrhi -unattended -nopause -stdout Result file: `/tmp/gigline/theatre-import.txt`. """ @@ -22,7 +22,7 @@ import sys import unreal -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ue_common # noqa: E402 SRC = "/tmp/gigline/theatre" @@ -52,7 +52,7 @@ def log(message: str) -> None: def import_pieces() -> list[str]: if not os.path.isdir(SRC): - log(f"MISSING {SRC} — run Tools/theatre_trim_build.py first") + log(f"MISSING {SRC} — run Tools/blender/theatre_trim_build.py first") return [] files = sorted(f for f in os.listdir(SRC) if f.lower().endswith(".fbx")) if not files: diff --git a/Tools/ue_vehicle_import.py b/Tools/unreal/ue_vehicle_import.py similarity index 97% rename from Tools/ue_vehicle_import.py rename to Tools/unreal/ue_vehicle_import.py index dedab69..2ddfe1c 100644 --- a/Tools/ue_vehicle_import.py +++ b/Tools/unreal/ue_vehicle_import.py @@ -1,9 +1,9 @@ # Import the sedan systems as separate static meshes (0.3.0). # -# blender -b --factory-startup --python Tools/vehicle_build.py -- \ +# blender -b --factory-startup --python Tools/blender/vehicle_build.py -- \ # --out Art/blender/vehicle/gigline-sedan.blend --fbx /tmp/gigline/vehicle/sedan.fbx # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_vehicle_import.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_vehicle_import.py -nullrhi -unattended -nopause -stdout # # One asset per system, per the vehicle-artist skill's object split: body, glass, front # and rear lights, mirrors, and one wheel reused four times. The body FBX also carries diff --git a/Tools/ue_vehicle_materials.py b/Tools/unreal/ue_vehicle_materials.py similarity index 95% rename from Tools/ue_vehicle_materials.py rename to Tools/unreal/ue_vehicle_materials.py index 5b08bfb..a049018 100644 --- a/Tools/ue_vehicle_materials.py +++ b/Tools/unreal/ue_vehicle_materials.py @@ -1,13 +1,13 @@ # Rebuild the vehicle's materials from the Blender colours (0.3.0). # # blender -b "Art/blender/vehicle/hot hatch.blend" \ -# --python Tools/vehicle_from_template.py -- --out /tmp/gigline/hatch +# --python Tools/blender/vehicle_from_template.py -- --out /tmp/gigline/hatch # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ -# -script=/Tools/ue_vehicle_materials.py -nullrhi -unattended -nopause -stdout +# -script=/Tools/unreal/ue_vehicle_materials.py -nullrhi -unattended -nopause -stdout # # Why this exists: the template's car paint is a **legacy node graph**, so its colour never # reached the exported FBX and the car arrived in Unreal near-black while Blender showed it -# red. `Tools/vehicle_from_template.py` now reads those graphs and records the colours; this +# red. `Tools/blender/vehicle_from_template.py` now reads those graphs and records the colours; this # replaces the imported material assets in place with plain constant-colour materials, so # the mesh slots pick them up by path without anything being re-imported. # diff --git a/Tools/ue_vehicle_probe_map.py b/Tools/unreal/ue_vehicle_probe_map.py similarity index 97% rename from Tools/ue_vehicle_probe_map.py rename to Tools/unreal/ue_vehicle_probe_map.py index 48935b0..8a7dbf5 100644 --- a/Tools/ue_vehicle_probe_map.py +++ b/Tools/unreal/ue_vehicle_probe_map.py @@ -1,7 +1,7 @@ """Add one piece of demo content to ReviewMap and save it as a vehicle probe level. GIGLINE_PROBE_ASSET=Terrain_DEM [GIGLINE_PROBE_NOCOLLISION=1] /UnrealEditor-Cmd \ - -run=pythonscript -script=/Tools/ue_vehicle_probe_map.py -nullrhi -unattended -nopause + -run=pythonscript -script=/Tools/unreal/ue_vehicle_probe_map.py -nullrhi -unattended -nopause Why: the Chaos pawn falls correctly on ReviewMap's empty floor and is thrown upward everywhere in DemoMap. Rather than guess which of DemoMap's 593 staged meshes is responsible, add them one group