diff --git a/Docs/Architecture/07_deployment_view.md b/Docs/Architecture/07_deployment_view.md index 4c08d31..09df818 100644 --- a/Docs/Architecture/07_deployment_view.md +++ b/Docs/Architecture/07_deployment_view.md @@ -7,7 +7,7 @@ | Dev workstation | Bazzite Linux 45 (Fedora 45 base, immutable ostree), Ryzen 7 7700X, 64 GB RAM, RTX 5070 Ti 16 GB, Vulkan RHI | | Engine install | `/run/media/system/DATA/Engines/UE_5.8.4` (73 GiB, external to repo) | | Project repo | `/run/media/system/DATA/Documents/06-games/gig-line` (Git + LFS, remote at `git.allucanget.biz`) | -| Build output | `/run/media/system/DATA/Builds/GigLine` (packaged Linux build, ~1.1 GiB) | +| Build output | `/run/media/system/DATA/Builds/GigLineQA/Linux` (packaged Development build, ~1.1 GiB; outside the repository) | Motivation Single-machine development. No build farm, no CI pipeline yet. The engine, toolchain, and GPU are all on the same host. diff --git a/Docs/Architecture/architecture.md b/Docs/Architecture/architecture.md index 9d4951f..2fef2ca 100644 --- a/Docs/Architecture/architecture.md +++ b/Docs/Architecture/architecture.md @@ -300,7 +300,7 @@ Source data: `Art/blender/` (`.blend` files), `Geo/` (OSM extracts, DEM, verific | Dev workstation | Bazzite Linux 45 (Fedora 45 base, immutable ostree), Ryzen 7 7700X, 64 GB RAM, RTX 5070 Ti 16 GB, Vulkan RHI | | Engine install | `/run/media/system/DATA/Engines/UE_5.8.4` (73 GiB, external to repo) | | Project repo | `/run/media/system/DATA/Documents/06-games/gig-line` (Git + LFS, remote at `git.allucanget.biz`) | -| Build output | `/run/media/system/DATA/Builds/GigLine` (packaged Linux build, ~1.1 GiB) | +| Build output | `/run/media/system/DATA/Builds/GigLineQA/Linux` (packaged Development build, ~1.1 GiB; outside the repository) | Motivation Single-machine development. No build farm, no CI pipeline yet. The engine, toolchain, and GPU are all on the same host. diff --git a/Docs/build.md b/Docs/build.md index df0ca4c..accf4a7 100644 --- a/Docs/build.md +++ b/Docs/build.md @@ -1,6 +1,6 @@ # Build -Fresh-checkout build procedure. Completed 2026-11-07 after the pinned engine was installed and a blank `Game/GigLine` build was packaged and launch-tested. +Fresh-checkout build, content-pipeline and packaging procedure for the walkable demo. Begun 2026-11-07 from a blank build and extended through the `0.1.x` patches; the release checks that exercise it are in [release-checks.md](release-checks.md). ## Constraints @@ -10,8 +10,7 @@ Fresh-checkout build procedure. Completed 2026-11-07 after the pinned engine was ## Engine -Pinned install: **Unreal Engine 5.8.4** (CL 58902052, promoted build) at -`/run/media/system/DATA/Engines/UE_5.8.4`. The engine bundles **no** Linux clang; the system clang (23.1.4, Fedora 45) is the game-code toolchain. Game targets must use `BuildSettingsVersion.V7` to share the installed editor's build environment. +Pinned install: **Unreal Engine 5.8.4** (CL 58902052, promoted build) at `/run/media/system/DATA/Engines/UE_5.8.4`. The engine bundles **no** Linux clang; the system clang (23.1.4, Fedora 45) is the game-code toolchain. Game targets must use `BuildSettingsVersion.V7` to share the installed editor's build environment. ## Fresh-checkout build @@ -42,20 +41,14 @@ cd /run/media/system/DATA/Engines/UE_5.8.4 Engine/Build/BatchFiles/RunUAT.sh BuildCookRun \ -project="/run/media/system/DATA/Documents/06-games/gig-line/Game/GigLine/GigLine.uproject" \ -noP4 -platform=Linux -clientconfig=Development \ - -cook -allmaps -build -stage -pak \ - -archive -archivedirectory="/run/media/system/DATA/Builds/GigLine" \ + -cook -build -stage -pak \ + -archive -archivedirectory="/run/media/system/DATA/Builds/GigLineQA" \ -unattended -# Expected: "BUILD SUCCESSFUL" (~1 min for the blank project; archive ~1.1 GiB) -# Launch test (plain, no -map): /run/media/system/DATA/Builds/GigLine/Linux/GigLine.sh +# Expected: "AutomationTool exiting with ExitCode=0 (Success)" and "EXIT=0", ~90 s +# Launch test (plain, no -map): /run/media/system/DATA/Builds/GigLineQA/Linux/GigLine.sh ``` -**Launch test must open the DemoMap.** A plain `./GigLine.sh` should load -`/Game/Hollywood/DemoMap` (set via `DefaultEngine.ini` `GameMapsSettings`). If it -instead opens `/Engine/Maps/Templates/OpenWorld`, the game shows an empty -non-interactive "stuck" scene — check that `GameDefaultMap` is in -`DefaultEngine.ini`, not `DefaultGame.ini`. Confirm the loaded map in the engine -log: `.../Linux/GigLine/Saved/Logs/GigLine.log` (`LoadMap: /Game/Hollywood/DemoMap`). -The archive directory is deliberately **outside** the repository; never archive into the checkout. +**Launch test must open the DemoMap.** A plain `./GigLine.sh` should load `/Game/Hollywood/DemoMap` (set via `DefaultEngine.ini` `GameMapsSettings`). If it instead opens `/Engine/Maps/Templates/OpenWorld`, the game shows an empty non-interactive "stuck" scene — check that `GameDefaultMap` is in `DefaultEngine.ini`, not `DefaultGame.ini`. Confirm the loaded map in the engine log: `.../Linux/GigLine/Saved/Logs/GigLine.log` (`LoadMap: /Game/Hollywood/DemoMap`). The archive directory is deliberately **outside** the repository; never archive into the checkout. ## Headless content checks @@ -76,9 +69,7 @@ Both content-acceptance tests run headless and are the template for all asset im ## Content pipeline (current) -The hero block and the demo level are built by a fixed sequence. Each step writes -a result file under `/tmp/gigline/`; read it, because the commandlet's own stdout -can be swallowed. +The hero block and the demo level are built by a fixed sequence. Each step writes a result file under `/tmp/gigline/`; read it, because the commandlet's own stdout can be swallowed. | Step | Tool | Output | | --- | --- | --- | @@ -86,21 +77,23 @@ can be swallowed. | Terrain | `Art/blender/terrain/terrain_from_dem.py` | `Art/blender/terrain/terrain-*.blend` | | 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` | six FBXs, one per mesh | -| Hero façade | `Tools/hero_facade_build.py` | `Art/blender/hero/hero-facade-*.blend` + JSON report | +| 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` | | Review shots | `Tools/ue_build_review_map.py` + `Tools/render_screenshots.sh` | `Screenshots/` | +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). + Two rules that the pipeline enforces rather than documents: -- **One conversion.** Metres to Unreal centimetres happens only in - `Geo/tools/osm_common.py::to_unreal_local`. Sidecars (`.geo.json`) record - the result and `Geo/tools/asset_metadata.py audit` re-derives it. -- **Collision is decided at import,** not later: terrain, road and sidewalks are - complex-as-simple; kerbs and props stay simple. A simple hull over a kerbed - street ramps straight across the kerb. +- **One conversion.** Metres to Unreal centimetres happens only in `Geo/tools/osm_common.py::to_unreal_local`. Sidecars (`.geo.json`) record the result and `Geo/tools/asset_metadata.py audit` re-derives it. +- **Collision is decided at import,** not later: terrain, road and sidewalks are complex-as-simple; kerbs and props stay simple. A simple hull over a kerbed street ramps straight across the kerb. ## Known engine quirks (2026-11-07) @@ -110,20 +103,35 @@ Two rules that the pipeline enforces rather than documents: ## Packaged Linux build -- 2026-11-07: blank `Game/GigLine` Development build packaged via the command above; 1.1 GiB archive at `/run/media/system/DATA/Builds/GigLine/Linux`; `GigLine.sh` launched on the desktop session, Vulkan swapchain created, 759 frames rendered across a 60 s smoke test. Controls document to be written with the first playable (`0.1.0` Gameplay section). +Current QA archive: `/run/media/system/DATA/Builds/GigLineQA`. A plain `./GigLine.sh` opens `/Game/Hollywood/DemoMap`; controls are in [controls.md](controls.md) and shown in-game bottom-right. The packaged log is written **relative to the wrapper's working directory**, i.e. `/Linux/GigLine/Saved/Logs/GigLine.log` — not into the repository. + +Behaviour check without a human at the keyboard: + +```bash +cd /run/media/system/DATA/Builds/GigLineQA/Linux +./GigLine.sh -windowed -ResX=1280 -ResY=720 -nosplash -qaselftest +# then read /Linux/GigLine/Saved/Logs/GigLine.log for "GigLine QA:" lines +``` + +`-qaselftest` walks for 2 s, resets, pauses/unpauses, sweeps a 16-row seam transect, fires a ground-referenced 21 m wall probe along the street, fires 40 m façade-occlusion traces sideways from the road centre wherever a building is staged, and writes a frame-time series to `Saved/Profiling/`. Every phase logs `... PASS`/`FAIL` and the run ends with `RESULT`. + +## Traps that cost real time in `0.1.x` + +| Symptom | Cause | Fix | +| --- | --- | --- | +| A builder reports success but the asset is unchanged | `.blend`/`.umap` are LFS-tracked and check out **read-only**; Blender and Unreal fail the save quietly | `chmod u+w ` first — `ue_build_*`/`ue_hero_facade_import` and every Blender builder need it | +| `File format is not supported` when reopening a `.blend` | `hero_facade_build.py --out` writes the **FBX**; putting the `.blend` path there overwrites it with FBX bytes | `--out /tmp/…fbx --save-blend Art/…blend --report Art/…json` | +| `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`) | ## 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. +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/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. -- The DemoMap is PIE-ready as committed: `GlobalDefaultGameMode` + - `GameDefaultMap=/Game/Hollywood/DemoMap`, and the map stages a PlayerStart at - `(-13336,-1150,600)` on the cut sidewalk with the walkable `AGigLineCharacter`. + - 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. +- The DemoMap is PIE-ready as committed: `GlobalDefaultGameMode` + `GameDefaultMap=/Game/Hollywood/DemoMap`, and the map stages a PlayerStart at `(-13336,-1150,600)` on the cut sidewalk with the walkable `AGigLineCharacter`. diff --git a/Docs/controls.md b/Docs/controls.md index 62c5a5e..591aec2 100644 --- a/Docs/controls.md +++ b/Docs/controls.md @@ -1,36 +1,30 @@ # Controls (`0.1.0` walkable demo) -Keyboard and mouse. The same list is shown in-game, bottom-right of the frame -(`AGigLineHUD`), and the packaged Linux build ships with these bindings. +Keyboard and mouse. The same list is shown in-game, bottom-right of the frame (`AGigLineHUD`), and the packaged Linux build ships with these bindings. | Action | Key | Notes | | --- | --- | --- | | Move forward | `W` | 4 m/s walk | -| Move back | `S` | | -| Strafe right | `D` | | -| Strafe left | `A` | | +| Move back | `S` | | +| Strafe right | `D` | | +| Strafe left | `A` | | | Look | Mouse | Captured on launch; `Escape` releases it | -| Jump | `Space` | | +| Jump | `Space` | | | Reset to start | `R` | Returns to the grounded spawn point | | Pause / release mouse | `Escape` | Second press resumes and recaptures the mouse | ## Notes -- 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`. +- 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`. ## 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_test.py` injects OS-level keyboard/mouse events through a virtual `/dev/uinput` device (see `Tools/input_emulation.py`), e.g.: ```bash python3 Tools/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. +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/odbl-review.md b/Docs/odbl-review.md index e3f7f52..2d33ba1 100644 --- a/Docs/odbl-review.md +++ b/Docs/odbl-review.md @@ -37,22 +37,12 @@ Important nuance: ODbL governs the **database** (the OSM data as a collection an ## 4. Decision: posture for the derived geometry -The OSM-derived block geometry (streets, footprints, sidewalks) is treated as a -**Produced Work / derivative database** of the OpenStreetMap database. +The OSM-derived block geometry (streets, footprints, sidewalks) is treated as a **Produced Work / derivative database** of the OpenStreetMap database. -- The **share-alike obligation is accepted for the data layer only**. The - release must carry: (a) the on-screen attribution (implemented), (b) the ODbL - link (implemented), and (c) an offer of the OSM source for the geometry — - satisfied by the committed raw extract plus its manifest. -- **Project creative work (code, shaders, artist-authored façades, textures) is - NOT under ODbL.** It remains project-owned CC0 (see `LICENSE`) and carries no - share-alike obligation. Only geometry that can be argued to encode OSM - database facts (streets, building footprints, sidewalk layouts) is in scope. -- **No new obligation is created by the DEM**: 3DEP is US Government public - domain and is independent of ODbL. -- **Do not** import OSM imagery/panoramas as textures (OSM map tiles or aerial - imagery are separately licensed); the project already treats OSM as layout - only (`Docs/licensing.md` rule 3). +- The **share-alike obligation is accepted for the data layer only**. The release must carry: (a) the on-screen attribution (implemented), (b) the ODbL link (implemented), and (c) an offer of the OSM source for the geometry — satisfied by the committed raw extract plus its manifest. +- **Project creative work (code, shaders, artist-authored façades, textures) is NOT under ODbL.** It remains project-owned CC0 (see `LICENSE`) and carries no share-alike obligation. Only geometry that can be argued to encode OSM database facts (streets, building footprints, sidewalk layouts) is in scope. +- **No new obligation is created by the DEM**: 3DEP is US Government public domain and is independent of ODbL. +- **Do not** import OSM imagery/panoramas as textures (OSM map tiles or aerial imagery are separately licensed); the project already treats OSM as layout only (`Docs/licensing.md` rule 3). Release checklist items this lands: @@ -63,20 +53,13 @@ Release checklist items this lands: ## 5. Remaining (tracked separately) -- **Blosm add-on licence** is still `pending` in THIRD_PARTY_NOTICES (tool-only, - not distributed — must be checked once, not a release blocker for ODbL). -- Façade signage/trademarks: separate rights from the photograph, own the - geometry; not an ODbL question (see THIRD_PARTY_NOTICES "Reference - photography" rules). +- **Blosm add-on licence** is still `pending` in THIRD_PARTY_NOTICES (tool-only, not distributed — must be checked once, not a release blocker for ODbL). +- Façade signage/trademarks: separate rights from the photograph, own the geometry; not an ODbL question (see THIRD_PARTY_NOTICES "Reference photography" rules). ## 6. Sources checked - ODbL text: - OSM licence summary: -- ISPRS/ODbL practice notes on derived game geometry from OSM footprints - (objective standard: attribution + produced-work offer, share-alike limited - to the data layer). +- ISPRS/ODbL practice notes on derived game geometry from OSM footprints (objective standard: attribution + produced-work offer, share-alike limited to the data layer). -Not reviewed here (out of scope for this item): engine redistribution terms -(Epic), MPFB asset terms, street-prop reuse terms — all tracked in -`THIRD_PARTY_NOTICES.md`. +Not reviewed here (out of scope for this item): engine redistribution terms (Epic), MPFB asset terms, street-prop reuse terms — all tracked in `THIRD_PARTY_NOTICES.md`. diff --git a/Docs/release-checks.md b/Docs/release-checks.md index fae4af0..bf30856 100644 --- a/Docs/release-checks.md +++ b/Docs/release-checks.md @@ -34,21 +34,18 @@ Measured at 1440p on the development workstation: Bazzite, Ryzen 7 7700X, 64 GB ## Check-record template -| Version | Check | Result | Evidence | Date | -| ------- | ----------------------- | ------ | -------- | ---- | -| `0.1.0` | Traversal, both sides | PASS — Blender pre-check + in-engine swept transect, 0 holes, ground Z 317..395 | `-qaselftest`, TODO 0.1.0 notes | 2026-11-08 | +| Version | Check | Result | Evidence | Date | +| --- | --- | --- | --- | --- | +| `0.1.0` | Traversal, both sides | PASS — Blender pre-check + in-engine swept transect, 0 holes, ground Z 317..395 | `-qaselftest`, TODO 0.1.0 notes | 2026-11-08 | | `0.1.0` | Placement vs references | PASS (with deliberate simplifications) | Section below, 2026-11-08 | 2026-11-08 | | `0.1.0` | Collision and packaging | PASS — no blocking collision, no invisible walls, no seam gaps (see audit note) | `-qaselftest` on the packaged build, 2026-11-08 | 2026-11-08 | -| `0.1.0` | 60 FPS at 1440p | PASS — 2.42 ms avg (414 fps), p99 3.01 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.1.0-walk-frametimes.csv` | 2026-11-08 | -| `0.1.0` | Fresh checkout build | PASS — clean clone of `main` (`4bc41e9`), `git lfs pull` + `check-lfs.sh` OK (130 LFS files), both `GigLineEditor` and `GigLine` targets built with the documented engine | `/tmp/fresh-check`, 2026-11-08 | 2026-11-08 | -| `0.1.0` | Packaged Linux build | PASS — packaged Development build launches straight into DemoMap, no missing-asset/linker errors; controls shown in-game and in `Docs/controls.md` | `Docs/perf/0.1.0-packaged-hud-controls.png`, 2026-11-08 | 2026-11-08 | +| `0.1.0` | 60 FPS at 1440p | PASS — 2.42 ms avg (414 fps), p99 3.01 ms at 2560×1440, TSR on, VSync off, no FG | `-qaselftest` perf capture, `Docs/perf/0.1.0-walk-frametimes.csv` | 2026-11-08 | +| `0.1.0` | Fresh checkout build | PASS — clean clone of `main` (`4bc41e9`), `git lfs pull` + `check-lfs.sh` OK (130 LFS files), both `GigLineEditor` and `GigLine` targets built with the documented engine | `/tmp/fresh-check`, 2026-11-08 | 2026-11-08 | +| `0.1.0` | Packaged Linux build | PASS — packaged Development build launches straight into DemoMap, no missing-asset/linker errors; controls shown in-game and in `Docs/controls.md` | `Docs/perf/0.1.0-packaged-hud-controls.png`, 2026-11-08 | 2026-11-08 | ## `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/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) @@ -80,78 +77,43 @@ probed from the DemoMap (commandlet, 2026-11-08). - **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/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) Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselftest`. -- **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.) -- **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). +- **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.) +- **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). ## `0.1.0` — 60 FPS at 1440p — record (2026-11-08) -**Build under test:** packaged `GigLineQA` Development, UE **5.8.4**, Vulkan SM5, -NVIDIA 615.78.08 (RTX 5070 Ti), Bazzite, Ryzen 7 7700X, 64 GB RAM. +**Build under test:** packaged `GigLineQA` Development, UE **5.8.4**, Vulkan SM5, NVIDIA 615.78.08 (RTX 5070 Ti), Bazzite, Ryzen 7 7700X, 64 GB RAM. -**Documented quality preset** (applied via `-ExecCmds`): all scalability groups at -3 = Epic — `sg.ViewDistanceQuality 3, sg.ShadowQuality 3, -sg.GlobalIlluminationQuality 3, sg.ReflectionQuality 3, sg.PostProcessQuality 3, -sg.TextureQuality 3, sg.EffectsQuality 3, sg.FoliageQuality 3, sg.ShadingQuality 3`. +**Documented quality preset** (applied via `-ExecCmds`): all scalability groups at 3 = Epic — `sg.ViewDistanceQuality 3, sg.ShadowQuality 3, sg.GlobalIlluminationQuality 3, sg.ReflectionQuality 3, sg.PostProcessQuality 3, sg.TextureQuality 3, sg.EffectsQuality 3, sg.FoliageQuality 3, sg.ShadingQuality 3`. -**Upscaling state (recorded from the run):** `r.AntiAliasingMethod=4` → **TSR on**, -default screen percentage, `r.VSync=0` (**off**), frame generation **off/unavailable** -(`r.Streamline.FrameGeneration.Mode` absent → -999). +**Upscaling state (recorded from the run):** `r.AntiAliasingMethod=4` → **TSR on**, default screen percentage, `r.VSync=0` (**off**), frame generation **off/unavailable** (`r.Streamline.FrameGeneration.Mode` absent → -999). -**Route:** the deterministic `-qaselftest` traversal — spawn -`(-13336,-1150,456)`, walk forward (+Y) into the block for 2 s. Frame times are -captured every frame over that route (829 frames), not a single sample. +**Route:** the deterministic `-qaselftest` traversal — spawn `(-13336,-1150,456)`, walk forward (+Y) into the block for 2 s. Frame times are captured every frame over that route (829 frames), not a single sample. **Result at 2560×1440:** -| Metric | Value | -| --- | --- | -| Frames captured | 829 | -| Average | **2.42 ms → 414 fps** | -| p50 | 2.38 ms | -| p95 | 2.71 ms | -| p99 | 3.01 ms (≈332 fps) | -| Max | 4.00 ms | +| Metric | Value | +| --------------- | --------------------- | +| Frames captured | 829 | +| Average | **2.42 ms → 414 fps** | +| p50 | 2.38 ms | +| p95 | 2.71 ms | +| p99 | 3.01 ms (≈332 fps) | +| Max | 4.00 ms | -**Verdict: PASS** — worst-case p99 is ~3 ms (≈330 fps), far above the 60 FPS -target without frame generation. +**Verdict: PASS** — worst-case p99 is ~3 ms (≈330 fps), far above the 60 FPS target without frame generation. -**Variance note:** two back-to-back runs measured ~9.2 ms (≈108 fps) when another -game instance / busy compositor overlapped the capture; repeated clean runs -(windowed and fullscreen, Wayland and X11 drivers, window focused) all measured -~2.4–2.5 ms. The high outlier is recorded here rather than hidden. +**Variance note:** two back-to-back runs measured ~9.2 ms (≈108 fps) when another game instance / busy compositor overlapped the capture; repeated clean runs (windowed and fullscreen, Wayland and X11 drivers, window focused) all measured ~2.4–2.5 ms. The high outlier is recorded here rather than hidden. **Reproduce:** @@ -165,8 +127,7 @@ Committed frame-time series: `Docs/perf/0.1.0-walk-frametimes.csv`. ## `0.1.0` — Fresh checkout build — record (2026-11-08) -Followed the documented steps in [`Docs/build.md`](build.md) exactly, into a clean -directory with no prior state: +Followed the documented steps in [`Docs/build.md`](build.md) exactly, into a clean directory with no prior state: ```bash git clone https://git.allucanget.biz/zwitschi/gig-line.git /tmp/fresh-check @@ -179,35 +140,23 @@ Engine/Build/BatchFiles/Linux/Build.sh GigLine Linux Development \ -project=/tmp/fresh-check/Game/GigLine/GigLine.uproject -WaitMutex ``` -- Clone of `main` at `4bc41e9`; LFS objects fetched (`DemoMap.umap` is real UE - package content, not a pointer). -- `GigLineEditor` → **Result: Succeeded** (28.2 s); produced - `/tmp/fresh-check/Game/GigLine/Binaries/Linux/libUnrealEditor-GigLine.so`. +- Clone of `main` at `4bc41e9`; LFS objects fetched (`DemoMap.umap` is real UE package content, not a pointer). +- `GigLineEditor` → **Result: Succeeded** (28.2 s); produced `/tmp/fresh-check/Game/GigLine/Binaries/Linux/libUnrealEditor-GigLine.so`. - `GigLine` (game) → **Result: Succeeded** (26.9 s). -- Engine: documented external install at `/run/media/system/DATA/Engines/UE_5.8.4` - (UE 5.8.4), system clang; no machine-specific paths beyond the documented - `-project=` argument. +- Engine: documented external install at `/run/media/system/DATA/Engines/UE_5.8.4` (UE 5.8.4), system clang; no machine-specific paths beyond the documented `-project=` argument. **Verdict: PASS.** ## `0.1.0` — Packaged Linux build + documented controls — record (2026-11-08) -**Package:** `RunUAT BuildCookRun … -platform=Linux -clientconfig=Development --cook -build -stage -pak -archive -archivedirectory=/run/media/system/DATA/Builds/GigLineQA` -→ `EXIT=0` (Success). +**Package:** `RunUAT BuildCookRun … -platform=Linux -clientconfig=Development -cook -build -stage -pak -archive -archivedirectory=/run/media/system/DATA/Builds/GigLineQA` → `EXIT=0` (Success). -**Launch:** `./GigLine.sh -windowed` (plain, no map argument) → -`LogLoad: LoadMap: /Game/Hollywood/DemoMap?Name=Player` — the packaged game opens -the walkable demo directly (the default-map fix holds in a package, not just the -editor). No `LogLinker`/missing-asset/failed-to-find errors in the run. +**Launch:** `./GigLine.sh -windowed` (plain, no map argument) → `LogLoad: LoadMap: /Game/Hollywood/DemoMap?Name=Player` — the packaged game opens the walkable demo directly (the default-map fix holds in a package, not just the editor). No `LogLinker`/missing-asset/failed-to-find errors in the run. **Controls documented and shown in-game:** -- `Docs/controls.md` — full binding table (WASD, mouse look, Space, R, Esc) plus - how to test input headlessly. -- `AGigLineHUD` now draws a control legend bottom-right every frame: - "WASD move · Mouse look · Space jump · R reset · Esc pause". -- Verified in the packaged capture: the bottom-right text bbox is 426×12 px - (a single right-aligned line), attributed text bottom-left. Screenshot: - `Docs/perf/0.1.0-packaged-hud-controls.png`. + +- `Docs/controls.md` — full binding table (WASD, mouse look, Space, R, Esc) plus how to test input headlessly. +- `AGigLineHUD` now draws a control legend bottom-right every frame: "WASD move · Mouse look · Space jump · R reset · Esc pause". +- Verified in the packaged capture: the bottom-right text bbox is 426×12 px (a single right-aligned line), attributed text bottom-left. Screenshot: `Docs/perf/0.1.0-packaged-hud-controls.png`. **Verdict: PASS.** diff --git a/Docs/setup.md b/Docs/setup.md index 33631bd..5b769a6 100644 --- a/Docs/setup.md +++ b/Docs/setup.md @@ -83,13 +83,24 @@ Without `git lfs install`, adding an LFS-matched file does not error — it comm Sizes: the engine lives outside the repository (pinned install path recorded in `toolchain.md`). The repository holds the Unreal project, Blender sources, geo sources, and tooling. -**Fresh checkout not yet tested end to end.** The build procedure in [build.md](build.md) was produced from a working tree, not from a clean clone. Exercise it before `v0.1.0` is tagged — it is a release check on the board. +**Fresh checkout verified 2026-11-08.** A clean clone of `main` with `git lfs pull` built both `GigLineEditor` and `GigLine` against the documented external engine (recorded in [release-checks.md](release-checks.md)). Re-run it before a release tag if the engine pin or the module's dependencies change. + +### Optional keys (local only) + +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 | +| `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. ## Known blockers | Blocker | Owner | Needed for | | --- | --- | --- | | Delete the fallback 5.2.0 engine archive (`/run/media/system/DATA/Downloads/Linux_Unreal_Engine_5.2.0.zip`) once the 5.8.4 pin is trusted through asset production | agent | Disk hygiene (19.6 GiB), pin safety | -| Fresh-checkout build exercised against the documented procedure | agent | `v0.1.0` release check | +| ~~Fresh-checkout build exercised against the documented procedure~~ | — | Done 2026-11-08 | Closed 2026-11-07: Epic account and engine download, QGIS `lts` removal, Blosm and MPFB on Blender 5.2 (headless invocation fix in `Art/blender/README.md`), engine validation on Bazzite, full project bootstrap. Earlier closed: Bazzite update and reboot, driver and Vulkan verification, `git-lfs`/`clang`/`cmake`/`ninja` layering. diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap index 0041601..86b0f4d 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:bcda8f7eacbbc8c20ff768177dc1c35863fd1bf403df142722880ca426e8a8f1 -size 54509 +oid sha256:3d9a16b41394efe0b040d919d1ffa8c9f06b5967cdb4a4306c3a04aa49fdba10 +size 54464 diff --git a/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset b/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset index 466c4da..a0e205b 100644 --- a/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset +++ b/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76cdd4770b34206e7b53c27d1902fde238efde555580ce08cf87270882613b85 +oid sha256:2b506386f8694a3788ec68d0f1feb9b4a6fa604282014d593514e3cefaa247f4 size 142674 diff --git a/MEMORY.md b/MEMORY.md index 1b9bd8d..a126f8c 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -507,3 +507,21 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. scan: banners, windows and the parapet produce different edge sets at different heights and the pairwise match is meaningless (it reported a 418 px "drift"). Verify it geometrically in the camera list instead. + +## 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. +- 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. diff --git a/TODO.md b/TODO.md index 432bcfa..3a219ec 100644 --- a/TODO.md +++ b/TODO.md @@ -13,10 +13,10 @@ Actionable task list for this project. This file is the single, central place fo Suggested blender skills: **export-pipeline** and **unreal-export** (scale/pivot/normals/materials/collision verification) if any packaging defect traces back to the Blender export presets. -### Patch tasks +### Patch tasks - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `packaging-setup-0.1.3`) -- [ ] Fix any packaging or build issues encountered in `0.1.x`. -- [ ] Update `Docs/` setup instructions for anything that changed. +- [x] Fix any packaging or build issues encountered in `0.1.x`. **Done 2026-11-09.** New `Tools/ue_common.py`: `make_writable()` clears the read-only bit an LFS `.umap` checks out with, and `require_saved()` turns a silent `save_map(False)` into a logged failure. `ue_hero_facade_import.py` and `ue_import_north_buildings.py` now call it before saving (verified from a forced `0o444` map: chmod note logged, `save_map: True`, exit 0); the map builders already did. `Docs/build.md` also gained the current pipeline table (7 site FBXs, frontage, north, hero, map), the archive path `Builds/GigLineQA`, the packaged-log location, the `-qaselftest` usage, and a **traps** table covering the read-only save, the hero `--out`-is-FBX mistake, the MPFB add-on path, the shot actor quitting under QA, and the null-RHI trace trap. +- [x] Update `Docs/` setup instructions for anything that changed. **Done 2026-11-09.** `Docs/setup.md`: fresh-checkout build marked verified (2026-11-08) with the blocker closed, plus a new _optional keys_ section (`OPENROUTER_API_KEY`, `MAPBOX_ACCESS_TOKEN`, both from the gitignored `.env`, neither needed by build/tests/release). `Docs/build.md`: header reframed for the demo, packaging command and archive corrected to `Builds/GigLineQA`, packaged-log path documented. `Docs/Architecture/07` + `architecture.md`: build-output path corrected and marked outside the repo. - [ ] Tag `v0.1.3` and write `CHANGELOG.md` entry; then remove this section. ## 0.1.4 — Proper building façades (patch) diff --git a/Tools/README.md b/Tools/README.md index efd3dca..988b14a 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -16,6 +16,7 @@ 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 | +| `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. | | `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 | diff --git a/Tools/tests/test_ue_common.py b/Tools/tests/test_ue_common.py new file mode 100644 index 0000000..e2d2b28 --- /dev/null +++ b/Tools/tests/test_ue_common.py @@ -0,0 +1,70 @@ +"""Tests for Tools/ue_common.py — the shared Unreal commandlet helpers. + +Pure stdlib: these helpers exist to turn two silent failures into loud ones, so +they are tested without starting Unreal. +""" + +import os +import stat +import sys +import tempfile +import unittest + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.insert(0, os.path.join(ROOT, "Tools")) + +import ue_common # noqa: E402 + + +class MakeWritableTest(unittest.TestCase): + def test_read_only_file_is_made_writable(self): + # Exactly how an LFS-tracked .umap checks out: 0o444. + with tempfile.NamedTemporaryFile(delete=False) as handle: + path = handle.name + try: + os.chmod(path, 0o444) + self.assertFalse(os.access(path, os.W_OK)) + note = ue_common.make_writable(path) + self.assertIsNotNone(note) + self.assertIn("chmod u+w", note) + self.assertTrue(os.access(path, os.W_OK)) + finally: + os.chmod(path, 0o644) + os.unlink(path) + + def test_writable_file_reports_nothing(self): + with tempfile.NamedTemporaryFile(delete=False) as handle: + path = handle.name + try: + os.chmod(path, 0o644) + self.assertIsNone(ue_common.make_writable(path)) + finally: + os.unlink(path) + + def test_missing_file_raises(self): + # A typo must not look like a permissions problem. + with tempfile.TemporaryDirectory() as tmp: + with self.assertRaises(FileNotFoundError): + ue_common.make_writable(os.path.join(tmp, "absent.umap")) + + def test_read_bits_are_preserved(self): + with tempfile.NamedTemporaryFile(delete=False) as handle: + path = handle.name + try: + os.chmod(path, 0o400) + ue_common.make_writable(path) + mode = os.stat(path).st_mode + self.assertTrue(mode & stat.S_IRUSR) + finally: + os.chmod(path, 0o644) + os.unlink(path) + + +class RequireSavedTest(unittest.TestCase): + def test_passes_the_save_result_through(self): + self.assertTrue(ue_common.require_saved(True, "/Game/X")) + self.assertFalse(ue_common.require_saved(False, "/Game/X")) + + +if __name__ == "__main__": + unittest.main() diff --git a/Tools/ue_common.py b/Tools/ue_common.py new file mode 100644 index 0000000..998646a --- /dev/null +++ b/Tools/ue_common.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Small shared helpers for the Unreal commandlet scripts. + +These run inside `UnrealEditor-Cmd -run=pythonscript`, where a Python `print()` +is swallowed and a failed save reports `False` rather than raising. Both are traps +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__))) + import ue_common + + ue_common.make_map_writable("/run/media/.../Content/Hollywood/DemoMap.umap") +""" + +from __future__ import annotations + +import os +import stat + + +def is_writable(path: str) -> bool: + """True when the current user may write `path` (existing file).""" + return os.access(path, os.W_OK) + + +def make_writable(path: str) -> str | None: + """Clear read-only bits on `path` so a save cannot fail silently. + + Content is LFS-tracked and `.umap`/`.blend` check out read-only, which makes + `EditorLoadingAndSavingUtils.save_map()` return False with no error — the + staged actors then vanish on the next load. Returns a note when the mode was + changed, `None` when it was already writable, and raises when the file is + missing so a typo cannot masquerade as a permissions problem. + """ + if not os.path.exists(path): + raise FileNotFoundError(path) + mode = os.stat(path).st_mode + if mode & stat.S_IWUSR: + return None + os.chmod(path, mode | stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | + stat.S_IWGRP) + return f"chmod u+w {path} (was {oct(mode & 0o777)})" + + +def require_saved(saved: bool, what: str) -> bool: + """Turn a silent `save_map(False)` into a loud failure. + + Use right after a save call: `ue_common.require_saved(saved, LEVEL)`. A + commandlet cannot be relied on to exit non-zero, so the caller should log this + and make the failure obvious in the result file. + """ + return bool(saved) diff --git a/Tools/ue_hero_facade_import.py b/Tools/ue_hero_facade_import.py index b36fd6c..49e4e8b 100644 --- a/Tools/ue_hero_facade_import.py +++ b/Tools/ue_hero_facade_import.py @@ -12,8 +12,14 @@ # centreline, from the street profile), and the hero's east edge at # X = -12024.5 cm (projected control point in local-origin.md). +import os +import sys + import unreal +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import ue_common # noqa: E402 + FBX = "/tmp/gigline/hero-facade.fbx" DEST = "/Game/Hollywood/ElCapitan" ACTOR_LABEL = "Hero_Facade" @@ -56,6 +62,19 @@ def import_fbx(): unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) +def _make_map_writable(level: str) -> None: + """Clear the read-only bit an LFS .umap checks out with. + + Without this `save_map()` returns False and the staged actor is lost on the + next load — no exception, no non-zero exit (see MEMORY: 0.1.x trap). + """ + content = unreal.Paths.project_content_dir() + rel = level[len("/Game/"):] if level.startswith("/Game/") else level + umap = os.path.join(content, rel + ".umap") + note = ue_common.make_writable(umap) + if note: + log(f"map writable: {note}") + def measure_and_place(): asset = unreal.EditorAssetLibrary.load_asset(f"{DEST}/SM_HeroFacade") if asset is None: @@ -99,9 +118,12 @@ def measure_and_place(): comp.set_static_mesh(asset) comp.set_mobility(unreal.ComponentMobility.STATIC) log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}") + _make_map_writable(LEVEL) saved = unreal.EditorLoadingAndSavingUtils.save_map( unreal.EditorLevelLibrary.get_editor_world(), LEVEL) log(f"save_map {LEVEL}: {saved}") + if not ue_common.require_saved(saved, LEVEL): + log(f"FAILED to save {LEVEL} — staged content will not persist") try: diff --git a/Tools/ue_import_north_buildings.py b/Tools/ue_import_north_buildings.py index 35c959f..ee5b820 100644 --- a/Tools/ue_import_north_buildings.py +++ b/Tools/ue_import_north_buildings.py @@ -15,6 +15,9 @@ import os import unreal +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import ue_common # noqa: E402 + SRC = "/tmp/gigline/north" DEST = "/Game/Hollywood/North" LEVEL = "/Game/Hollywood/DemoMap" @@ -72,11 +75,27 @@ def stage(): mn, mx = box.min, box.max log(f"{label}: mesh world-space X {mn.x:.0f}..{mx.x:.0f} Y {mn.y:.0f}..{mx.y:.0f} size " f"{mx.x-mn.x:.0f}x{mx.y-mn.y:.0f} cm") + _make_map_writable(LEVEL) saved = unreal.EditorLoadingAndSavingUtils.save_map( unreal.EditorLevelLibrary.get_editor_world(), LEVEL) log(f"save_map {LEVEL}: {saved}") + if not ue_common.require_saved(saved, LEVEL): + log(f"FAILED to save {LEVEL} — staged content will not persist") +def _make_map_writable(level: str) -> None: + """Clear the read-only bit an LFS .umap checks out with. + + Without this `save_map()` returns False and the staged actor is lost on the + next load — no exception, no non-zero exit (see MEMORY: 0.1.x trap). + """ + content = unreal.Paths.project_content_dir() + rel = level[len("/Game/"):] if level.startswith("/Game/") else level + umap = os.path.join(content, rel + ".umap") + note = ue_common.make_writable(umap) + if note: + log(f"map writable: {note}") + def main(): for fn, asset_name in IMPORTS: task = unreal.AssetImportTask()