# THE DESCENT (Unreal port) - STATUS Consolidated 2026-10-05 after the merge of the core, the world track (M1) and the carrier track (M2) and the integration pass (`Docs/status/integ.md`). Everything below was run on this PC and the numbers are measured. The track notes it replaces stay in `Docs/status/` as history (`core.md`, `world.md`, `carrier.md`, `integ.md`): they hold the long lists of deviations and guesses. Design: `DESIGN_UE.md`. Requirements: `BRIEF_UE.md`. **State (2026-10-06):** build PASS, 0 warnings; `import_assets.ps1` PASS; **78 automation tests PASS; 74 registered scenarios (72 pass, 2 fail on purpose)**; full chain run 2026-10-06 after M10. Milestones done: M0, the core, M1, M2, the UI kit and HUD, **M3** (board, launch, dive, swim, dock; section 11), the **first-person arm** (section 12) and the **fix list after the Critique of M1-M3** (section 13). **M4-M10 are done (sections 14-20); section 21 is how to play**: one line per milestone in `Docs/status/port_progress.md`. Sections 1-10 below describe the state after M2; where they say "M3 not started", sections 11-13 win. ## 1. What exists | Area | Where | |---|---| | Build, import, tests, scenario runner | `Tools\build.ps1`, `import_assets.ps1`, `run_tests.ps1`, `run_scenario.ps1`, `export_kazah.ps1` | | Harness: JSON state dumps, shots with luminance sidecar, scenarios, console commands | `Source\TheDescent\Debug\` | | Sim core (12 ordered steps + 7 late steps, clock, pause, FastForward, intent latch), boot registry, seams (env query, HUD model, stream focus, game classes) | `Core\`, `UI\DescentHudModel.h`, `UI\DescentHudSubsystem.*` | | World definition, density function (SDF), routes, Shear | `World\DescentWorldDef.*`, `DescentSDF.*`, `DescentRoutes.*`, `DescentShear.*` | | Terrain: 20 m chunks, surface nets on workers, streaming, carve, cleanup, queries, slices | `World\DescentTerrainSubsystem.*`, `SurfaceNets.*`, `TerrainChunk.h`, `DescentTerrainActor.*`, `DescentWorldSlice.*` | | Ore deposits (600), `mined` counters | `World\DescentOreSubsystem.*` | | Look: environment actor (water, fog, sun, exposure, bloom), light pool, lamps | `Render\DescentEnvironment.*`, `DescentLightPool.*`, `DescentLamps.*` | | Carrier (CANTICLE) blockout: 16 rooms, 15 doors, 7 windows, hull, moon pool, 6 exterior lights, dry volume, stream focus | `Vehicles\CarrierLayout.*`, `CarrierMeshBuilder.*`, `CarrierBuild.*`, `CarrierBlockout.*`, `DescentCarrier.*` | | Character, controller (Enhanced Input made in C++), interaction, modes Title and Foot | `Player\` | | Fixture stubs (helm, chart table, beds, nutrient ports, vat, packer, storage, terminal, carousel console, ladder) | `Systems\DescentFixture.*` | | Kazah Tender (parts, rotors, headlamp) and the showcase rig | `Vehicles\KazahTender.*`, `KazahLayout.inl`, `Debug\DescentShowcaseRig.*` | | Assets by script | `Tools\ue\assets_10` (Kazah materials), `11` (Kazah meshes), `20` (MPC_Descent, M_DescentSurface, M_DescentGlass), `35` (carrier materials), `40` (M_Terrain), `41` (PP_Water), `90` (the map); `_helpers_look.py` | | Tuning constants (one header per namespace, no umbrella) | `Core\Tuning\DescentTune_{Sim,Env,World,Terrain,Look,Carrier,Player}.h` | | User launchers | `OpenEditor.bat`, `PlayGame.bat` | Generated and kept so the project rebuilds from text + source art: `SourceArt\Kazah\*.fbx` + `kazah_manifest.json`, `Vehicles\KazahLayout.inl`, `Content\**` (from the import), the map `/Game/Maps/Descent_Main` (only a PlayerStart; everything else is spawned by code at `StartPlay`). ## 2. Commands and how long they take PowerShell, non-interactive, one PASS/FAIL line, exit 0/1. Prefix `powershell -NoProfile -ExecutionPolicy Bypass -File`. They find the project from their own location (any copy). **Every build needs `UE_SDKS_ROOT=D:\HADAL - The Descent\.tools\AutoSDK`** (the scripts set it; no Visual Studio is registered). Since 2026-10-11 it is also set permanently for the Windows user account. | Task | Command | Measured | |---|---|---| | Build | `Tools\build.ps1` (`-Verbose`) | 5-12 s incremental, ~100 s full module; log `Saved\Logs\build_last.log` | | Import assets | `Tools\import_assets.ps1` (`-Force`, `-ForceMap`, `-Only `) | 7 s unchanged, ~26 s `-Force` (the 970k-triangle Kazah hull took 52 s the very first time) | | Tests | `Tools\run_tests.ps1 [-Filter Descent.World]` | 15 s for all | | Scenario | `Tools\run_scenario.ps1 -Name [-TimeoutSec 300] [-ResX 1280 -ResY 720] [-Fps 30] [-RealTime]` | 7-25 s each (engine start-up ~6 s) | | Package (the 64-bit game .exe) | `Package.bat` or `Tools\package.ps1 [-Config Development] [-SkipCook] [-Out ]`; into `..\Builds\Win64-Shipping` | 2.7 min, 0.76 GB (2026-10-14); log `Saved\Logs\package_last.log`. What is cooked: `Config\DefaultGame.ini` (maps, folders the code loads by name); a new such folder or map needs a line there | `run_scenario.ps1` starts `UnrealEditor.exe -game -windowed -RenderOffscreen -unattended ... -UseFixedTimeStep -FPS=30 -DescentScenario=`, stops **only the process it started** at the limit, prints the PNGs this run wrote with their sidecar luminance numbers, the state dumps, the last error lines and `SCENARIO PASS|FAIL`. PASS = exit code 0 and the log line `DESCENT_SCENARIO_RESULT PASS`. Fixed step: every frame advances the world by exactly 1/30 s. The engine's ToolsetRegistry plugin logs a Python traceback (`PythonTestRunner`) at every start; it is not ours and the script filters it. After **any change to `MPC_Descent` run a full `import_assets.ps1`, not `-Only`**: a changed parameter list gives new parameter ids and the materials built earlier fail to compile. A normal start (no scenario) was run: boot report "8 step(s) ran, all ok", no errors or warnings of ours. ## 3. Console commands (all `Descent.`, typed in `~` or `S.Exec(...)`; a failure logs `DESCENT_CMD_FAIL` and fails a running scenario) | Command | Effect | |---|---| | `DumpState ` | `Saved\State\.json` | | `Shot ` / `ShotRegion x0 y0 x1 y1` / `ShotRing cx cy r0 r1` | `Saved\Shots\.png` + `.json` sidecar; mean luma of a rectangle (fractions) / ring (pixels), appended to the sidecar | | `Teleport x y z`, `Camera x y z pitch yaw roll [fov]`, `Scenario `, `ListScenarios`, `Sections`, `Quit [code]` | metres and degrees; `Quit` is ignored in the editor | | `Sim.FastForward [dt]`, `Sim.Pause <0/1>`, `Player.Intent fwd right up lookYawDps lookPitchDps secs [primary secondary sprint]` | the sim core | | `World.Goto ` | start canyon_mid wellA shelfA needle h1 bore void kilo7 drop shelfC hairpin undercroft antiphon gullet goal abyss hollow honest grotto sieve spur_e1 (`carrier:` is M3) | | `World.Density x y z`, `.Carve x y z r`, `.Cleanup x y z r`, `.NearestDeposit [type]`, `.ClearRoute <1..3>` | results in the log and in `terrain.last_*` / `ore.query` | | `World.Slice [mpp=2]` | map PNG over the claim box: rock white, water black, carved grey, veins in ore colours, route axes red | | `Look.Set ` | look switches for tests (`Snow` only stores the switch: marine snow is not built) | | `Player.Place `, `.PlaceLocal x y z yaw`, `.MoveTo `, `.Look yaw pitch`, `.Use`, `.Mode` | the character in carrier-local metres; MoveTo walks with real intent | | `Carrier.Motion fwd right up yawDps pitchAmp pitchPeriod roll`, `.Stop` | debug motion of the carrier (used by `m2_moving_base`) | | cvars | `descent.Look.ExposureEV` (0.3), `descent.Look.Bloom` (1), `descent.Terrain.StreamRadius` (160), `descent.Carrier.LampScale` (1) | (`Descent.Carrier.Helm` also exists, registered by another track's file; not covered here.) ## 4. State dump (`Saved\State\.json`) `{ "schema": 1, "name": ..., "
": {...}, ... }`, sections alphabetical, **metres, m/s, degrees**, numbers rounded to 4 decimals. Providers run on the game thread and may get a null `World`. Sections: `meta camera pawn world` (built in), `sim boot ui(hud) env_query stream` (core), `world_def` (seed, counts, hash, build_ms), `terrain` (chunks known/filled/meshed/modified, triangles, pending_jobs, components_active, apply/remesh/fill/mesh ms, last_density, last_removed, floating_after_cleanup), `ore` (deposits, mined_total, loose 0, crystals 0, query), `env` (state Water/Dry, surface_light, fog_density, volumetric_fog, exposure_ev, bloom_intensity/threshold, snow_instances_visible 0, temperature_c, sun_on, water_pp, light_pool_lit), `carrier`, `player` (mode, room, local_m, world_m, ref point, focus, view), `tenders`. Add one with `FDescentStateProviderRegistrar(TEXT("fuel"), [](UWorld*, FJsonObject& Out){...})`; several files share one section with `FDescentStatePartRegistrar(TEXT("ui"), TEXT("widgets"), ...)`. ## 5. Scenarios DSL (any `Debug\Scenarios_.cpp`): `DESCENT_SCENARIO(name, "what") { S.TimeLimit(60.f); S.Do(label, fn); S.Until(label, fn); S.WaitFrames(n); S.WaitSeconds(s); S.WaitSim(s); S.FastForward(s); S.Exec(cmd); S.Camera(loc, rot, fov); S.LookAt(loc, target, fov); S.Shot(label); S.Expect(label, fn); S.ExpectNear(label, fn, expected, tol); S.DumpState(label); S.Quit(); }`. `FDescentScenarioContext` gives `World Debug Sim() Snapshot() LastShotStats SetNamed/GetNamed Fail`. Steps run one per frame. `S.Shot` waits for shaders, then 10 frames and 0.75 s, and **fails on a black frame** (< 0.1 % of pixels above 8/255) or a wrong size. Every scenario starts with a resolution check; boot steps run under a scenario (the first mode is Foot at once). New steps are free functions over the builder in your own header: `World\DescentWorldScenarioSteps.h` has `EnsureFlyPawn/PlaceView/ViewAt` (possess a fly pawn: the default pawn is the character now), `WaitStreamIdle`, `ShotAllowDark` (like Shot but a dark frame is the point), `SpawnTestSquare`, `MeasureFrames` (logs a PERF line). Shot sidecar: `{w,h,mean_luma,centre_luma,non_black,p99_luma,mean_rgb,regions}` on sRGB-encoded values (luma = .2126 R + .7152 G + .0722 B). | Scenario | What it proves | Result | |---|---|---| | `smoke`, `core_sim` | harness and every command; the sim under the engine's tick | PASS | | `kazah_showcase`, `kazah_closeups` | the Tender whole, lit by the real environment and its own headlamp | PASS | | `m1_world_slices` | 8 world maps, 600 deposits | PASS | | `m1_terrain_tour` | 9 places: stream idle, <= 900k triangles, lit rock; PERF lines | PASS | | `m1_carve` | vein, 5 carves, ore counters, cleanup, wireframe at a chunk border | PASS | | `m1_commands` | every `World.*` command in a running game | PASS | | `look_calibrate`, `look_deep_lamp`, `look_deep_dark`, `look_near_wall`, `look_throat`, `look_bloom`, `look_dry_mask` | the luminance bands of DESIGN 7.10, bloom (U12), the dry mask (U6) | PASS | | `look_rooms` | 16 rooms on foot, each 0.10-0.45, env Dry and 26.0 C in each | PASS | | `m2_walk` | real-input walk bridge to bay, 16 room shots, fixture prompts | PASS | | `m2_dry` | U5, U6 end to end: dry, 26.0, CABIN text, env Dry, no volumetric fog, interior pixels identical with the water on or off, the sea in the windows fogged | PASS | | `m2_exterior` | the hull from 85 m is a dark mass, six lights | PASS | | `m2_moving_base` | R2: walking on the moving, pitching carrier (no fallback needed) | PASS | | `m12_boot` | a normal start, nothing placed: Foot, bridge, Dry 26.0, carrier at the start pose, terrain meshed around it, water outside; 4 shots | PASS | | `look_sweep` | not a test: wall luma for a range of exposures (how `ExposureEV` was chosen) | PASS | | `selftest_timeout`, `selftest_expect` | **fail on purpose** (timeout exit 2; an `ExpectNear` that names label and numbers) | FAIL as intended | ## 6. Automation tests (49, `-nullrhi`, names `Descent..`) Boot (`GameClasses Order`), Carrier (`Layout DryVolume Temperature Geometry Route`), Env (`QueryDefaults WaterTemperature DryVolumeProvider`), Harness (7), Hud (`Model Subsystem`), Kazah (`Layout RotorSpin`), Look (`SurfaceLight LightPoolSelect LampSetup`), Math (`DatumAndUnits Helpers Rng Noise3`), Player (`Body GameClasses`), Sim (6), Stream (`Focus`), Terrain (`SeamMatch CarveOre Cleanup NoBounds Queries`), World (`Determinism Counts Places NoBypass CarrierFit Shear`). `FDescentTestWorld TW; TW.Sub()` gives a throw-away game world with the real subsystems driven by hand. ## 7. The core: how a system plugs in (Core\, section 9 below for the rules) * **Sim** (`UDescentSimSubsystem`): `Step(dt)` clamps dt to [0, 0.05], latches the intent, runs the registered callbacks of 12 entries in enum order (`Intent Carrier Carousel Tenders TenderTool Vitals Terrain Ore Cargo Fauna Sonar Horror`), then `LateStep` runs `InteractionFocus Environment LightPool MarineSnow Audio HudModel Dashboards`. `RegisterStep(EDescentSimStep::X, [this](const FDescentSimFrame& F){...}, {Owner, SubOrder, RateHz})`; order inside an entry is by `SubOrder`, then `Owner`, never by registration. `FastForward(seconds, dt)` loops `Step` in one frame. `SetPaused` stops the tick function, not `Step()`. A subsystem that registers declares `Collection.InitializeDependency()`; **register in `OnWorldBeginPlay`**, not `Initialize`, when a test world must have no callback (the terrain does). * **Intent**: `FDescentIntent { Move (X fwd, Y right, Z up), Look (degrees PER SECOND), bPrimary bSecondary bSprint bJump bDown }`; `SetLiveIntent` (controller), `SetDebugIntent(i, simSeconds)` wins; systems read `GetIntent()`. * **Boot** (`DESCENT_BOOT_STEP(Name, Descent::Boot::Order::X)` in your own .cpp): `CoreSeams 0, BuildWorldDef 100, SpawnEnvironment 200, SpawnTerrainActor 210, SpawnCarrier 220, SpawnTenders 300, SpawnFixtures 310, SpawnStartPlaceables 320, StreamInitial 400, CreateUi 450, PlaceCharacter 500, EnterFirstMode 510, LoadSave 600, AfterAll 900`. `ADescentGameMode::StartPlay` calls `Super` first, then runs the registry (also under a scenario). Section `boot` lists what ran. * **Game classes**: `DESCENT_REGISTER_GAME_CLASS(Pawn, 100, ADescentCharacter)` (slots Pawn, PlayerController, HUD, GameState, PlayerState; highest priority wins). The fly pawn is priority 0. * **Environment query**: `UDescentEnvQuerySubsystem::RegisterDryVolume(Owner, fn(WorldUU), temperature = 26)`; `Descent::IsDry(World, UU)`, `Descent::AmbientTemperatureAt(World, UU)` (water formula `4.2 + 9 clamp((Depth-1740)/1400) + (Depth > 2300 ? 6 : 0)` outside). The carrier registers its hull; the environment and the HUD ask. * **HUD model** (`FDescentHudModel` via `UDescentHudSubsystem::Model()`): gameplay writes in the `HudModel` late step, the UI track reads. The player controller writes `Mode, Prompt, DepthM/Text, TemperatureC/Label/Text`; the rest (band, vitals, vehicle panel, compass) is for the owning milestones. * **Stream focus**: the view location is always a focus; others register `RegisterFocus/RegisterActorFocus(Owner, Actor, radiusM)`. The carrier registers 120 m; the terrain unions the spheres. * **Math** (`Core\DescentMath.h`): `Clamp Lerp Smooth Damp SMin DatumM(1780) DepthOfZ ZOfDepth`, `FRng` (v2 mulberry32, bit-exact), `FNoise3` (v2 simplex, checked). **Units**: metres in the design, 1 m = 100 uu, convert only with `Descent::MetresToUU/UUToMetres`. ## 8. World, terrain, look, carrier: the facts to know **World** (`FDescentWorldDef::Get()`, built once in 13 ms from fixed seeds, hash `64a8cc5b4634536c`): canyon X -250..850, 4 wells (radius 80), 8 designed chambers + 16 worm pockets, galleries B (552 m) and S (1159 m), 11 Tender tubes, 2 collapse shelves, 2 pinches, 2 rings, the Sieve (sponge), 900 pores, 36 worm tunnels, 600 deposits (372 ferrite, 174 cobalt, 54 pearl), 8 beacons, 22 named places. Three carrier routes with `ClearRoute` (the CarrierFit test). `FDescentShear::VelocityAt` is the Throat's current (10 m/s at Z +65). There is **no position clamp anywhere** (U7); a chunk 100 km out can be queried, filled and carved. **Terrain**: chunk 20 m, voxel 1.25 m, padded 20^3; classification lazy; fill + mesh on workers (12 in flight), apply <= 1.5 ms/frame, carved chunks remeshed synchronously (2.5 ms); no physics collision, rock is queried: `Density Gradient Raycast PushOutSphere Carve CleanupFloating`. `FlipWinding = true` in the mesher (the v2 index order is culled otherwise). Measured: fill 1.1-1.25 ms (target 0.6), mesh 0.1 ms, tour frame 2.4-3.4 ms, game thread <= 2.3 ms with up to ~500 components, 103k-298k triangles (cap 900k), 241-597 chunks (cap 900). **The risk R1 fallback (grouping chunks) was not needed.** **Look**: `ADescentEnvironment` (spawned at boot) is the **only authority for exposure, fog, post-process and the sun**; nothing else (scenarios, the showcase rig) brings an environment. It owns the volumetric-only height fog (faded out over 0.3 s when the camera is dry), one unbound post-process component (manual exposure `descent.Look.ExposureEV` 0.3, no physical camera exposure; bloom 0.15 x `descent.Look.Bloom`, threshold 1.2, size 2.0; vignette, grain, fringe), the surface sun (1.6 x L, L(Z) = exp(-(40-Z)/68)), the 24-light pool, and writes `MPC_Descent` every late step. `PP_Water` (blendable "before DOF") turns distance into water; **pixels with custom stencil 1 are spared only when the camera itself is in dry air and the stencil object is the visible surface** (custom depth equals scene depth: custom depth is drawn without the main depth test). Lamps: `Descent::Look::ConfigureSpot` (no inverse-square falloff, exponent 3, R/cone/colour/intensity of DESIGN 7.5): the fly pawn's headlamp and the Tender's headlight. Calibration: wall luma 0.474 / 0.305 / 0.080 at 10 / 20 / 40 m at EV 0.3. Materials: `M_DescentSurface` (BaseColor Metallic Roughness EmissiveColor EmissiveStrength AmbientMix + the water ambient term), `M_DescentGlass`, `M_Terrain` (strata, ore facets and glints, noise normal), `PP_Water`. Not built: marine snow, caustics, the fake environment cube, `PP_ScreenFX`, hex basalt jointing. **Carrier** (`ADescentCarrier`, procedural blockout, no model yet): start pose (0, 0, -30) yaw 0, +X bow; layout in `FLayout` (rooms, doors, windows, 15 station anchors, route graph, `IsDryLocal`, `RoomAt`); hull lathe with the moon-pool cut; every interior primitive has custom stencil 1 and lighting channel 1 only; 6 exterior lights; a dry volume (26.0 C); `OverrideMesh` strings let real models replace rooms. Interior brightness is calibrated to the real exposure: lamp unit 4.6, emissive 1.36, ambient 0.0255 (`Tune::Carrier`, `assets_35`; the carrier track's original 2700 / 800 / 15 were for a scenario exposure times `InteriorExposureFactor` 0.0017). If the exposure changes, scale the three numbers by the same factor and re-run `import_assets.ps1`. **Player**: `ADescentCharacter` (capsule on the moving carrier: `bFastAttachedMove`, `bStayBasedInAir`, base velocity, tick prerequisite on the sim), `ADescentPlayerController` (modes Title, Foot; intent to the sim), interaction focus and `E` on fixtures (stubs answer with a toast naming the milestone). A character outside the dry volume hovers (swimming is M3). ## 9. Conventions for parallel work (Three or more coders in separate copies; the Administrator merges with a three-way file merge) No two tracks edit the same file: every system plugs in from its own files. | What you add | Where | Never edit | |---|---|---| | Sim callbacks | `RegisterStep/RegisterLateStep` from your subsystem/actor | `DescentSimSubsystem.*` | | Boot step | `DESCENT_BOOT_STEP` in your .cpp | `DescentGameMode.*`, `DescentBoot.*` | | Pawn/controller/HUD class | `DESCENT_REGISTER_GAME_CLASS` | `DescentGameMode.cpp` | | World subsystem | derive from `UDescentWorldSubsystem` (game and PIE worlds only) | - | | Dry space, temperature, stream points, HUD data | the seams above | their files | | Console commands | `Debug\DescentCommands_.cpp`, `FAutoConsoleCommandWithWorldAndArgs`, helpers from `DescentCommandUtil.h` | other systems' files | | Scenarios, tests | `Debug\Scenarios_.cpp`; `Tests\Tests.cpp` (`DESCENT_TEST_FLAGS`) | others' | | State sections | `FDescentStateProviderRegistrar` / `FDescentStatePartRegistrar` | `DescentStateRegistry.*` | | Assets | `Tools\ue\assets__.py` with `run(ctx)`, safe to re-run: 10-19 vehicles, 20-49 world/look/carrier, 50-79 others, 90 the map | other modules | | Tuning | a new `Core\Tuning\DescentTune_.h`, included directly | other namespaces' headers | | Module dependency | your own marked block in `TheDescent.Build.cs` | the shared lists | | Status notes | `Docs\status\.md`; `STATUS.md` is edited by the Administrator's merge (and by this pass) | `BRIEF_UE.md`, `DESIGN_UE.md` | * **Unity builds**: give file-local helpers and variables a prefix that carries the system (`CmdHelm`, `WorldDef...`, `M2...`); a non-unity build passes. * **Line endings**: keep a file's own (repository mixes CRLF and LF); new files are LF. * **Scenarios** possess a fly pawn explicitly if they need one; they never place an environment. The map is empty (a PlayerStart); a scenario that takes a picture needs something lit in it (the carrier and terrain exist at boot). ## 10. Open, guessed, not run * **Design changes made at the merge** (details in `world.md`, `integ.md`): route data of DESIGN 2.10 for routes 2 and 3 (the hull does not fit the Designer's numbers); beacons b7/b8 moved out of rock; carrier interior brightness rescaled; the showcase rig lost its own fog and exposure; the Tender headlight is the DESIGN 7.5 spot; `m2_exterior` camera at 85 m and a hull-region assertion (a hull 180 m away is fogged away by the real water). * **Open in the look**: volumetric light beams are in but were never visible as beams in any shot; marine snow is not built (`env.snow_instances_visible` is always 0); chunk fill misses its 0.6 ms target (1.1-1.25 ms). * **Carrier deviations** (`carrier.md`): start at (41.8, 0, 1.5); hull flat-shaded; a low invisible barrier at the pool gap until M3; the trace channel "Placement" is `ECC_GameTraceChannel1` by number (add `+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Ignore,bTraceType=True,bStaticObject=False,Name="Placement")` to `[/Script/Engine.CollisionProfile]` for the editor); mouse look assumed to deliver raw counts x 0.12 deg. * **Not run**: a real keyboard and mouse; pressing Play in an editor window (the `-game` start was run with `-RenderOffscreen`); the user's editor on the main project still holds the old module and must be reopened (new classes, the `ProceduralMeshComponent` plugin). While an editor holds the module DLL, `build.ps1` links `UnrealEditor-TheDescent-.dll`; never `-Rebuild` with an editor open. * **Later milestones** (design in `DESIGN_UE.md` section 10): M3 board, launch, dive, swim, dock; M4 mine (loose ore, suction, crystals); M5 unload/box/refine; M6 pilot the carrier; M7 build/craft; M8 survive; M9 sonar/map/dread/sound; M10 UI and look locked. The UI kit and HUD are another track's. Tender flight model, collision, dashboards, sounds: not started. * Several Kazah-part pivots (canopy hinge, hatch hinge, tool housing centre) are guesses (`SEPARATE_PARTS` in `export_kazah.py`). U10 (tool side) and the other user decisions of DESIGN 11.2 are open. ## 11. M3: board, launch, dive, swim, dock (details and deviations: `Docs/status/m3.md`) Three Kazah Tenders (BT-01 TENDER, BT-02 DART, BT-03 MOLE) hang in the bay carousel (`Vehicles\CarrierCarousel.*`: hub, three arms, hoist, launch, dock; `carrier.carousel` state). `Vehicles\TenderFlight.*` is the flight model (stats, energy, crush, honest voids, nine collision spheres against the density field and the analytic carrier hull `CarrierHullShape.*`); `KazahTender.*` has the fleet subsystem, cockpit camera (FOV 104.5 horizontal), the two dashboards, lids, paint. Player: swimming (`DescentCharacterMovement`), vitals (`DescentVitals.*`: O2, health, death, respawn, debt), the mode machine (`DescentPlayerModes.cpp`: Board, Docked, Launch, Sub, Dock, Swim, Ladder, Dead) and the UI keys (Tab PDA, M map, Esc/P/H pause, title card). Keys: E board / dock / leave / climb, Space launch, F lamp, W A S D Space C Shift fly and swim. Commands: `Descent.Tender.Board|Launch|Dock|Exit|Enter| Teleport|LookAt|Set|GiveOre`, `Descent.Carrier.Carousel`, `Descent.Player.Set|Swim|PoolDrop|Climb`. Tests `Descent.Tender.Stats`, `Descent.Shear.Field`, `Descent.Carousel.Clearance`, `Descent.Carrier.HullShape`, `Descent.Vitals.Rules`. Scenarios `m3_launch_dock`, `m3_carousel`, `m3_rotor`, `m3_no_walls` (U7), `m3_swim`, `m3_crush`, `m3_cockpit`, `m3_ui`. ## 12. The first-person arm (details: `Docs/status/arm.md`) `Player\Arm\*` (layout, poses, reach solver, jack-in sequence, `UDescentArmViewmodelComponent`), mounted on `ADescentCharacter` (up on foot and swimming, hidden in a Tender). Export `Tools\export_arm.ps1`, import `assets_12_arm.py`. Commands `Descent.Arm.Rig|Pose|Curl|Reach|ReachWorld|Jack|Recipe`. 8 tests `Descent.Arm.*`, scenarios `arm_showcase`, `arm_jack`, `arm_character`. **While the user's editor has the project open `import_assets.ps1` cannot save.** **Since 2026-10-11 the arm is off the player's view** (section 22: the Twinblast body is the view); `descent.Body.OldArm 1` or `Descent.Body.View arm` gives it back. ## 13. Fix list after the Critique of M1-M3 (details: `Docs/status/fixes_m3.md`) Rock in the lamp reads as rock (`M_Terrain` surface detail; new sidecar numbers `centre_detail`, `centre_rms`; scenario `look_rock`); the cargo display is whole in the pilot's view; the moon pool is dark water (its brightness had missed the 0.0017 rescale); the arm takes the light of the room the player is in (lighting channel 2 + `ArmRoomFill`); the HUD vehicle panel is cleared outside a Tender; HUD-on shots `m3_swim_hud_swim`, `m3_launch_dock_hud_piloting`. ## 14. M4 Mine (2026-10-06) One drill and suction tool per Tender (U10): LMB drills along the cockpit ray (`UTenderFlightComponent::StepTool`, `Vehicles/TenderTool.cpp`), RMB pulls loose ore (`UDescentLooseOreSubsystem`, pool of 160 chunks) into the hold; T swaps to the laser head once it is built. The cargo display beside the dash shows the hold as ore cubes (U11). Scenarios `m4_mine`, `m4_cargo_display`, `m4_tool`, `m4_meters`, `m4_basalt` PASS. Left for later: crystals on the veins, the laser beam, drill sparks and debris, a swimmer taking a loose chunk. ## 15. M5 Unload, box, refine (2026-10-06): the core loop closes `UDescentCargoSubsystem` (`Systems/DescentCargo.*`): a docked Tender's hold travels as glowing pieces through glass pipes to the packer in the cargo bay (U13); the packer makes boxes of six on four outfeed slots; E carries a box, E at the refinery's hopper loads it (U14); refined goods go to the carrier's storage; the terminal sells them against the debt. Tests `Descent.Cargo.PackerMath`, `.PipeRoute`, `Descent.Store.Ledger`; scenarios `m5_core_loop`, `m5_sell`, `m5_packer_full` PASS. Left for later: the hand intake hatch, the bio pipe, the boom animation. ## 16. M6 Pilot the carrier (2026-10-06) E at the helm: the mouse moves an aim point and the heavy hull follows (U4), W S is the lever, 2.0 m/s is reached within seconds (U9: 1.93 m/s at 4 s), A D lateral thrusters, Space C ballast, V five cameras, X all stop. The hull collides with the rock through its sample points and takes damage. `Vehicles/CarrierHelm.*`. Tests `Descent.Helm.Dynamics`, `Descent.Routes.BlockedBeforeDig`, `ReachGoal_1`, `ReachGoal_3` PASS; scenarios `m6_helm_speed`, `m6_helm_mouse`, `m6_scrape`, `m6_walk_moving` PASS. `Descent.Routes.ReachGoal_2` failed at first (stalled over a well) and was fixed in M10 (section 20). ## 17. M7 Build and craft (2026-10-06) * **Build mode (U1)**, `Systems/DescentBuild.*`: B opens the kit menu, the ghost follows the eye (green valid, red with the reason on the HUD), 0.25 m grid (Alt free), Q / E / wheel turn it in 15 degree steps, LMB places, RMB or Esc cancels, **holding E for 0.8 s on a placed object picks it up** (a short E uses it). Six validity rules as pure functions (`Descent::Build::Validate`), plus a real overlap test against walls, props, objects and the player. The fabricator (fabrication room) and the refinery are placed objects from the start. Kits: fabricator, refinery, locker (+40 storage), rack, arm Mk I / II, work light, table, chair, shelf, couch, bunk. * **Fabricator (V8)**, `Systems/DescentCraft.*`: the 31 recipes of section 6.7 in six tabs; a recipe takes exactly its cost from storage and the ore boxes; Tender upgrades apply to all three Tenders. Suit inventory of 24 cells, hotbar 1-9 (in the PDA: select a cell, press 1-9), greyed weapon slot (V11). * **Automation (U14)**, `Systems/DescentAutomation.*`: Box Rack (4 sockets, rank 1) and Handling Arm Mk I / II (reach 3.2 / 4.5 m, 4.0 / 2.5 s per box); an arm takes the lowest-rank box some higher-rank socket in reach accepts (outfeed 0, rack 1, hopper 2), so boxes never loop. E on an arm is its power. * **Save**, `Systems/DescentSave.*`: slot `descent_0`; pause window SAVE, title card CONTINUE, `Descent.Save` / `Descent.Load`. Player, economy, upgrades, kits, placed objects, boxes, packer, carrier, Tenders, modified terrain chunks (int16, zlib) and the ore counters. * Commands: `Descent.Build.Give|Ghost|Aim|Rotate|Place|Cancel|Pickup|Spawn`, `Descent.Craft `, `Descent.Inventory.Give`, `Descent.Hotbar.Assign`. State sections `build`, `inventory`, `upgrades`, `save`, and `cargo.arms`, `cargo.racks`, `refineries[].room`. * Tests `Descent.Build.Rules`, `Descent.Recipes.Costs`, `Descent.Arms.Ranks` PASS; scenarios `m7_build`, `m7_arms`, `m7_craft`, `m7_inventory`, `m7_save` PASS. * Left for later: the ghost is an opaque glowing box, not a hologram; hand tools (flare, beacon, hand sonar, propulsor) are inventory items without a use yet; sleeping in a bunk; the couch, table and shelf of the habitation room are still fixed blockout; the carousel angle and an undocked Tender that is docked at load time are not restored; no autosave on dock. ## 18. M8 Survive (2026-10-06) * **Hunger (U2)**: nutrition drains 100 % in 40 minutes (`Descent::Vitals::NutritionDrainPerSec` = 0.0417 / s); at 0 health falls 1 / s. `m8_hunger`: 25.000 in 600 s. * **Biomass (U3)**, `Systems/DescentBio.*`: the reserve is finite (20 FU, starts at 6; 1 FU = 25 points). E at a wall port runs the existing arm plug sequence (4.8 s): nutrition rises to min(100, n + 25 x reserve), the reserve pays, 12 cr is billed. Below 0.1 FU the port refuses (red ring, radio line). The vat in the galley shows the reserve as its liquid level. Refill: fish sucked in by a Tender (RMB) fill its bio-tank (4 FU); docked, the tank empties through the glass bio pipe into the vat; a lantern polyp taken by hand while swimming (E) is loaded at the vat with E (+0.4 FU). * **Fauna (U6)**, `Life/DescentFauna.*`: 13 school homes (silverfin 0.10 FU, lanternfish 0.15 FU), boids, the six nearest homes within 220 m are simulated. No fish inside the hull: targets and spawns keep 6 m off, fish within 3 m are steered out, one found within 0.5 m is projected out in the same step; the moon-pool shaft counts as hull. `fauna.inside_hull` was 0 at every step of 120 s with the carrier parked on a school. * **Hand tools**, `Player/DescentTools.*`: LMB uses the selected hotbar item: flashlight, flare (thrown, burns 120 s, six at most), marker beacon (HUD and map). Sleep: E at a bed or a placed bunk (health 100, nutrition -10, refinery +120 s, Tenders charged, autosave). * Commands `Descent.Bio.Set`; state sections `bio`, `fauna`, `tools`. Scenarios `m8_hunger`, `m8_biomass`, `m8_no_fauna_inside`, `m8_tools` PASS. * Left for later: jellies, flora, polyp clusters only where a school home is; the handheld propulsor; no fade when sleeping; fish are stretched cubes. ## 19. M9 Sonar, map, dread, sound (2026-10-06) * **Sonar (V2)**, `Systems/DescentSonar.*`: R pings from the Tender (160 m, 260 m with the Sonar Module, 2,200 rays, 2.5 s), the helm (420 m, 4,200 rays) or swimming with the handheld sonar (90 m). 420 rays per sim step; a hit appears as the 170 m/s front reaches it and lives 20 s. The points are drawn by the HUD over the scene (projected each frame), so fog and rock never hide them. * **Survey map**: `UDescentMapSubsystem`, a 473 x 365 x 220 bitset marked by sonar hits, plus visited columns. PDA > SURVEY MAP (M): depth slice AUTO (reference depth +- 60 m), ALL or a fixed depth (`Descent.Map.Slice`), carrier and YOU, beacons, dead beacons, contacts, place names once known. * **Horror (V3)**, `Systems/DescentHorror.*`: hull creaks, pressure groans, the heartbeat from below (`horror.pulse`), the leviathan (40 segments, 152 m, passes 55-120 m ahead, answers a ping as red contacts, never touches anything), sonar ghosts, eight dead beacons whose logs type out on the radio and cut off with [SIGNAL LOST] (PDA > LOGS), whispers, the phantom and false radio when nutrition is under 25, and the ending (carrier anchored at the goal or `Descent.Carrier.Finish`: radio lines, end card after 16 s, play continues). PDA > STATUS is filled. * **Audio (V3)**, `Audio/DescentAudio.*`: 13 loops and 16 one-shots are synthesised in code at start (no asset, no import step) and played through procedural sound waves; volume, pitch and the master low-pass (5,200 Hz in air, 2,400 Hz in a Tender, 1,300 Hz swimming) are set every frame. The `audio` state section lists them, because scenarios run with `-nosound`. * Commands `Descent.Sonar.Ping`, `Descent.Map.Slice`, `Descent.Horror.Levi|Heartbeat|Phantom`, `Descent.Carrier.Finish`. State `sonar`, `map`, `horror`, `audio`. * Scenarios `m9_sonar`, `m9_leviathan`, `m9_beacon`, `m9_hallucination`, `m9_heartbeat`, `m9_audio`, `m9_ending` PASS. * Measured deviation: in the Great Void the standard 160 m ping finds 323 walls of 2,200 rays (the chamber is wider than the range); with the Sonar Module 1,620 points are alive after the second ping. The scenario checks the 1,500 with the module. * Left for later: light flicker, the screen glitch, the Floor glow and water pulse of the heartbeat, honest voids flagged red on sonar, the ping shell, the chart table as a map screen, sound was never listened to (rendered and wired, but every run here is `-nosound`). ## 20. M10 Interface complete (2026-10-06) * The HUD objective line names the next step of the loop; the pause window lists every key (17 rows) and has SAVE; the title card has CONTINUE. * Every window opens on the real game and is filled by its system: PDA (inventory, survey map, logs, status), fabricator, refinery, packer, storage (E at the storage wall), terminal, vat, build menu (B), end card. * Scenarios: `m10_ui_layout` (Foot, Sub, helm bridge, helm chase: no HUD regions intersect at 1280x720, 1000x560, 800x450), `m10_ui_windows` (14 windows fit 1280x720 and 800x450), `m10_bloom` (the pause slider drives `env.bloom_intensity`: 0 to 0.15, the top of the slider is the default), `m10_look` (16 rooms, mean luma 0.17 to 0.40), `m10_perf` (nine places at 1920x1080: 4.15 ms average, worst 4.86 ms, at most 668,790 terrain triangles), `m10_full_run` (board, launch, mine, dock, pipes, box, refinery, six ingots, one sold, route 1 dug, autopilot to its end, ending, end card). All PASS. * `Descent.Routes.ReachGoal_2` is fixed: over a well the autopilot now holds the heading the well was dug for and creeps onto its axis before it uses the ballast (`FCarrierAutopilot::Step`). All 78 automation tests pass. * Measured, not fixed: for the first seconds after a teleport of hundreds of metres the render thread takes 40-180 ms a frame (caches being built); `m10_perf` waits 5 s at each place before measuring. A player never teleports, but the first frames after a load may stutter the same way. * Left for later: the low preset; look tuning; two of the four bloom shots of the design (`sonar_bloom`, `vein_bloom`); the log is not scanned for errors by `m10_full_run` (a failed command fails it). ## 21. How to play the loop (keys) Play, click DIVE. On foot: W A S D, mouse, Shift, Space. The HUD's objective line says the next step. 1. **Board**: walk aft and down to the sub bay (corridor, stairwell, mid deck, bay stairs), E at the Tender that hangs at BOARD. **Dive**: Space launches. 2. **Mine**: mouse steers, W A S D and Space / C move, F headlamp, R sonar. LMB drills the glowing vein, RMB sucks the loose ore in (and fish, for the vat). 3. **Dock**: fly up under the moon pool, E. **Unload**: the hold travels through the glass pipes to the packer in the cargo bay by itself. E leaves the Tender. 4. **Box**: in the cargo bay E on an ore box carries it (G or LMB puts it down). **Refine**: E at the refinery's glowing slot loads it; ingots go to storage. 5. **Sell**: E at the Tethys terminal. **Build**: E at the fabricator (fabrication room) builds upgrades and kits; B opens the build menu, LMB places, Q / E / wheel turn, RMB cancels, holding E on a placed object picks it up. A Handling Arm between the packer outfeed and the refinery carries boxes alone. 6. **Helm**: E at the helm on the bridge; mouse aims, W S lever, A D thrusters, Space C ballast, V cameras, X all stop, R sonar, E leaves. 7. **Eat**: E at a nutrient port on the galley wall (deck 2); the vat beside it shows the reserve; refill it with fish (RMB in the Tender, then dock) or polyps taken while swimming (E) and loaded at the vat (E). Sleep: E at a bed. Tab PDA, M map, 1-9 hotbar, Esc pause and save. ## 22. The player's body: Paragon Twinblast (2026-10-11; details, numbers and what looks wrong: `Docs/status/player_body.md`) * **Update 2026-10-12 (first section of `player_body.md`):** the body now plays the user's Fab mannequin animations, retargeted and baked onto Twinblast's skeleton by `Tools\ue\assets_14_twinblast_anims.py` (`import_assets.ps1 -Only twinblast_anims`; 21 sequences in `Content\Player\Twinblast\Anims`): idle, walk, jog, jump, swim (still / forward / sides), jack-in reach, button push on "use". Ladder and sleep are not wired. Switches: `descent.Body.AnimSet 1|0` (0 = the pack's own set), `descent.Body.ArmsUp 0..1` (default 0, arms hang), `descent.View.Bob 0..1`. The view bob went from 8.7 / 12.5 cm (walk / sprint, peak to peak) to 0.9 / 1.4 cm, in step with the clip's footfalls; the body no longer trails the view by a frame when turning. Scenario `body_view_probe` measures it (log lines `VIEWPROBE`). Tests 83 of 83. Open: looking down does not show the legs; swimming shows no arms looking ahead. The text below describes 10-11 (set 0, arms up). * **Later on 2026-10-12 (second and third task, `player_body.md`):** the body is anchored under the camera in the own view and leans back when the player looks down (legs in the picture from -60 degrees); the jack-in hand goes to the port by IK. **The model is data now:** `Config\DescentBody.json` (profile + animation table) read by the game (`Player\Body\DescentBodyProfile.*`) and by the bake `Tools\ue\assets_14_body_anims.py` (`import_assets.ps1 -Only body_anims`), which makes `A_Body_` sequences per model; guide `Docs\PLAYER_MODEL_SWAP.md`. The sprint has its own clip (`MM_Run_Fwd`) with the play rate matched to the legs (planted-foot slide 120.6 -> 26.3 cm/s) and the lower body turned toward the running direction; the swim's arm stroke is made in code (`descent.Body.SwimArms`). New switches: `descent.Body.Model`, `.Anchor`, `.NeckBehindCm`, `.LookDownLeanDeg`, `.SwimArms`, debug `.DebugOldLoco`, `.DebugRunRole`. Scenarios `body_view_probe` (view numbers, look-down joints) and `body_run_look` (run clips side by side). Tests 84 of 84. * The player has a body: the default (red-armed) `TwinBlast` mesh of the user's Fab pack on `ADescentCharacter::GetMesh()`, seen in first person. **Nothing under `Content\ParagonTwinblast\` is edited**; ours is `Player\Body\*` (C++), `Core\Tuning\DescentTune_Body.h` and one generated material, `Content\Player\Twinblast\M_Twinblast_LegsMetal` (`Tools\ue\assets_13_twinblast.py`, `import_assets.ps1 -Only twinblast`). * `UDescentPlayerBodyComponent`: mesh scale 0.975 (the eyes of his standing pose at the 1.65 m camera), feet on the capsule bottom, no collision. In the player's own view the head and the front collar are hidden bones, the mesh is a first-person primitive (camera first-person scale 0.55), it stands a little behind the capsule axis (more when looking down) and the upper arms are turned up and inward so the hands are in the picture. Any other camera sees the whole man. Not drawn in a Tender. At the helm he stands with his arms down. * **No guns**: the pistol material section is hidden. `descent.Body.Guns 1` (or `Descent.Body.ShowGuns 1`) draws them. * **Machine legs**: a material on the lower-body slot of the component (plated steel, the arms' red on the bands). The shape is still a man's legs. * **Animation**: `UDescentBodyAnimInstance`, a C++ instance without an AnimBlueprint that samples the pack's own sequences from the real movement: Idle, Walk / Jog in four directions with the play rate matched to the speed, Jump_Start / Apex / Fall_Loop / Land, the fall loop slowed for swimming (the pack has no swim), Idle_Relaxed at the helm. The pack's `Twinblast_AnimBlueprint` is not used. Jack-in: nothing is played with the body (the mechanic and its 4.8 s are unchanged, `UDescentBioSubsystem`). * **The old arm is one switch away**: `descent.Body.OldArm 1` or `Descent.Body.View arm` (scenario `arm_character` takes it). * Tests `Descent.Body.AnimState`, `.Setup`, `.SeatedAndSwitch`, `.FollowsMovement` (82 tests in all, all PASS). Scenario `player_body` (33 pictures: first person on foot, looking down, walking, in a jump, against a wall, swimming, at the helm, in a Tender; third person front, sides, back, legs, feet, arms, guns on) PASS. State section `body`. Commands `Descent.Body.View|ShowGuns|ThirdPerson`. * `Tools\import_assets.ps1` no longer hangs after a finished import (it waited for an `adb` server the editor leaves behind).